Covers all commands (sync, deploy, register, adopt) and their supporting subsystems.
Run against a real Dataverse environment. Mark each test PASS / FAIL / SKIP with date and tester initials.
- CFG — Configuration
- BLD — Build
- DEP — Deploy
- REG — Register
- SYN — Sync
- ADO — Adopt
- STP — Step attributes
- IMG — Images
- IMP — Impersonation (RunAsSystem / RunAsUser)
- DRY — Dry run
- VRB — Verbose flag
- ERR — Error handling and exit codes
| ID | Description | Steps | Expected result |
|---|---|---|---|
| CFG-01 | Default config discovery | Run any command without --config from a project directory containing dvx.json. |
Tool loads config without error. |
| CFG-01b | Upward config search | Place dvx.json in the project root; run dvx wr sync without --config from a nested subdirectory (e.g. ./WebResources). |
Tool finds and loads the root dvx.json, and the command behaves identically to running from the root: relative config paths (webResources.folder, webResources.manifest, project) resolve against the config file's directory, so the sync completes. |
| CFG-02 | --config override |
Pass --config ./custom/config.json pointing to a valid file in a non-default location. |
Tool loads the specified file; default path is ignored. |
| CFG-03 | Config file not found | Pass --config ./does-not-exist.json. |
Exit 1. Error message includes the missing path and a suggestion to use --config. |
| CFG-04 | Config file is empty | Create a zero-byte config file; pass it via --config. |
Exit 1. Error message says file is empty or invalid. |
| CFG-05 | Config file has invalid JSON | Write { bad json to a file; pass it via --config. |
Exit 1. Error surfaces a JSON parse failure. |
| CFG-06 | Valid multi-environment config | Config has dev, uat, prod environments. Run --env uat. |
Tool connects to the uat URL, not dev or prod. |
| CFG-07 | Environment name is case-insensitive | Config defines "name": "Dev". Run with --env dev (lowercase). |
Tool finds the environment. |
| CFG-08 | Unknown environment name | Run --env nonexistent. |
Exit 1. Error message includes "nonexistent" and lists the available environment names. |
| CFG-09 | publisherPrefix from config |
Config has "publisherPrefix": "solu". Run sync or deploy without --publisher-prefix. |
Tool uses solu as the prefix. |
| CFG-10 | --publisher-prefix CLI overrides config |
Config has "publisherPrefix": "solu". Run with --publisher-prefix abc. |
Tool uses abc, ignoring the config value. |
| CFG-11 | Publisher prefix missing entirely | Config has no publisherPrefix; no --publisher-prefix passed. Run sync. |
Exit 1. Error message mentions publisherPrefix and --publisher-prefix. |
| CFG-12 | DVX_CLIENT_SECRET env var |
Set $env:DVX_CLIENT_SECRET = "my-secret". Remove clientSecret from config (or set it to a wrong value). |
Tool authenticates successfully using the env var value. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| BLD-01 | Valid project builds and emits nupkg | Run sync or deploy against a valid pac plugin init project. |
Output shows Built <ProjectName>.<version>.nupkg. Build succeeds. |
| BLD-02 | nupkg path prefers project-name match | Project has multiple .nupkg files under bin/Release. |
Tool picks the one whose stem starts with the project name. |
| BLD-03 | .csproj path not found |
Pass --project ./nonexistent.csproj. |
Exit 1. Error message includes the missing path. |
| BLD-04 | dotnet build compilation failure |
Introduce a compile error in the plugin source. Run sync. |
Exit 1. Error output includes the dotnet build failure details. |
| BLD-05 | No .nupkg produced |
Modify the .csproj so it does not emit a NuGet package. Run sync. |
Exit 1. Error message says no .nupkg found under bin/Release and mentions pac plugin init. |
| BLD-06 | DLL not found after build | Rename the assembly output in .csproj so the DLL name doesn't match the project name. Run sync. |
Exit 1. Error message says the expected .dll was not found. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| DEP-01 | Happy path — package exists, upload succeeds | Run deploy against an environment where the pluginpackage record already exists. |
Output shows the Uploading package content... sub-step and Deployed. Assembly ID: {id}. Exit 0. |
| DEP-02 | Plugin package not found in Dataverse | Run deploy when no pluginpackage record matching {prefix}_{assemblyName} exists. |
Exit 1. Error message includes the unique name and says "initial upload must be done once manually". |
| DEP-03 | Correct unique name formed | Config has publisherPrefix = "solu". Project name is MyPlugin. |
The tool looks up solu_MyPlugin in Dataverse. Verify via --verbose output or error message. |
| DEP-04 | Connection failure — bad credentials | Set an incorrect clientSecret in config. Run deploy. |
Exit 1. Error message references the failed Dataverse connection. With --verbose, inner exception details are visible. |
| DEP-05 | Upload rejected by Dataverse | Run deploy where the SDK Update fails (e.g. revoke the service principal's write privilege, or upload corrupt package content). |
Exit 1. Error surfaces the OrganizationServiceFault message. |
| DEP-06 | pluginassembly child record not found after upload | Dataverse processed the upload but has no child pluginassembly record (rare edge case). |
Exit 1. Error message mentions no pluginassembly child found and says to ensure initial package upload was fully processed. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| REG-01 | --project path — happy path |
Run register --env dev --project ./MyPlugin.csproj. Assembly exists in Dataverse. |
Builds project locally, finds assembly by name, runs step sync. Exit 0. |
| REG-02 | --assembly-name path — happy path |
Deploy assembly first. Run register --env dev --assembly-name MyPlugin. |
Downloads DLL from pluginassembly.content, runs step sync. Exit 0. |
| REG-03 | Neither --project nor --assembly-name |
Run register --env dev without either option. |
Exit 1. Error: "Provide either --project or --assembly-name." |
| REG-04 | Both --project and --assembly-name |
Pass both options together. | Exit 1. Error: "--project and --assembly-name are mutually exclusive." |
| REG-05 | --assembly-name — assembly not in Dataverse |
Pass a name that doesn't exist. | Exit 1. Error includes the assembly name and suggests running dvx plugin deploy first. |
| REG-06 | --assembly-name — sourcetype is not Database |
Assembly was uploaded with sourcetype != 0. |
Exit 1. Error says content bytes are not stored in Dataverse and suggests using --project instead. |
| REG-07 | --assembly-name — content field is empty |
pluginassembly.content is null/empty. |
Exit 1. Error says assembly has no content and suggests using --project instead. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| SYN-01 | Full happy path — first registration | Deploy package, then run sync with at least one [PluginStep]-decorated class. No existing steps. |
Build succeeds, package pushed, step(s) created. Output shows Created: N. Exit 0. |
| SYN-02 | Re-sync with no changes | Run sync twice with identical code. |
Second run shows Skipped: N (all existing steps already match). No new steps created, no steps deleted. |
| SYN-03 | Step property updated in-place | Change ExecutionOrder, Description, FilteringAttributes, or RunAsSystem on an existing [PluginStep]. Re-run sync. |
Output shows Updated: 1. The step GUID in Dataverse is unchanged. The new property value is visible in PRT. |
| SYN-04 | Step name change causes delete + create | Change Entity, Message, Stage, or Async on a [PluginStep]. Re-run sync. |
Output shows Deleted: 1, Created: 1. Old step is removed, new step created with new name. |
| SYN-05 | Orphan step deleted | Remove a [PluginStep] attribute from a class. Re-run sync. |
Output shows Deleted: 1. Step no longer exists in Dataverse. |
| SYN-06 | Entire plugin class removed | Delete a plugin class from the DLL. Re-run sync. |
All steps belonging to that class are deleted. |
| SYN-07 | IPlugin class with no [PluginStep] |
Add a class implementing IPlugin with no attribute. Run sync. |
Warning logged for the unattributed class. All other steps processed normally. Exit 0 (not exit 2). |
| SYN-08 | Multiple [PluginStep] on one class |
Decorate one class with two attributes (different entity/message). Run sync. |
Two separate steps created in Dataverse, both with the correct class GUID. |
| SYN-09 | No plugintype records found |
Deploy assembly, then run register before Dataverse has processed the pluginpackage. |
Exit 2. Error in output: "No plugintype records found for assembly…". No Create calls made. |
| SYN-10 | Unknown SDK message | Use [PluginStep("account", "NonExistentMessage", Stage.PostOperation)]. Run sync. |
Warning: "Unknown SDK message 'NonExistentMessage'". Step skipped. Other steps processed. |
| SYN-11 | Entity+message not filterable | Use a combination that has no sdkmessagefilter record in Dataverse (with an Entity set). |
Warning: "No sdkmessagefilter for entity … + message …". Step skipped. |
| SYN-14 | Entity-less (global) message | Register [PluginStep(Message = "Associate", Stage = Stage.PostOperation)] (no Entity). Run sync. |
Step created with sdkmessagefilterid empty. No "No sdkmessagefilter" warning. Verify in PRT the step targets the Associate message with no primary entity. |
| SYN-13 | Partial failure — some steps fail | Simulate by revoking write access to one entity. | Exit 2. Successful steps show in Created/Updated counts. Failed step shown in Errors. |
| SYN-15 | [CustomApi] class skipped silently |
Add a class implementing IPlugin marked [CustomApi] (a Custom API implementation, no [PluginStep]). Run sync (or register). |
The [CustomApi] class is skipped with no "no [PluginStep]" warning (contrast SYN-07). All other steps processed normally. Exit 0. |
Run against an assembly that already has steps registered in Dataverse but no
[PluginStep]attributes in the source.
| ID | Description | Steps | Expected result |
|---|---|---|---|
| ADO-01 | Happy path — scaffold attributes | Run adopt --project <csproj> against an assembly with hand-registered steps. |
[PluginStep(...)] attributes written onto the matching classes; using beyro.PluginAttributes; added where missing. Summary Adopted: N attribute(s)…. Exit 0. |
| ADO-02 | Dry run writes nothing | Run adopt … --dry-run. |
Planned attributes are printed; no source files are modified (git status clean). |
| ADO-03 | Idempotent re-run | Run adopt twice. |
Second run reports the steps as already present (skipped); no further file edits. |
| ADO-04 | Unmatched type reported | Ensure a registered step's class is absent/renamed in the project. Run adopt. |
Warning No source class found for '<Type>' — skipped. Other classes still written. Exit 0. |
| ADO-05 | Custom image alias preserved | Adopt a step whose pre/post image uses a non-default alias. | Generated attribute includes PreImageAlias = "<alias>" / PostImageAlias = "<alias>". |
| ADO-06 | Unsecure configuration imported | Adopt a step that has an unsecure configuration string. |
Generated attribute includes Configuration = "<value>". |
| ADO-08 | Round-trip — no churn on first sync | After adopt, run sync --dry-run. |
Output shows Updated: N, Created: 0, Deleted: 0 — existing steps adopted in place by identity. |
| ADO-09 | Assembly-name override | Run adopt --assembly-name <name> where the Dataverse assembly name differs from the project name. |
Tool reads steps from the named assembly. |
| ADO-10 | Assembly not found | Run adopt --assembly-name does-not-exist. |
Exit 1. Error message names the missing assembly. |
| ADO-11 | Entity-less (global) message adopted | Adopt an assembly with a hand-registered Associate / Disassociate step (no sdkmessagefilter). |
Generated attribute has an empty entity, e.g. [PluginStep("", "Associate", Stage.PostOperation)]. No "no entity / not representable" warning. Step is adopted, not skipped. |
| ADO-12 | Custom API not scaffolded; class marked [CustomApi] |
Adopt an assembly that backs a Custom API (plugin bound via customapi.plugintypeid; its step is registered at stage 30 / Main Operation). |
No [PluginStep] is written for the Custom API class; the class is marked [CustomApi] instead (with using dvx.PluginAttributes;). Summary reports N Custom API class(es) marked [CustomApi]. Standard event-plugin steps in the same assembly are still adopted. Exit 0. |
| ADO-13 | Custom API dry run | Run the ADO-12 scenario with --dry-run. |
Planned output shows … ← [CustomApi] for the Custom API class and no stage-30 [PluginStep] / (Stage)30. No source files modified (git status clean). |
| ADO-14 | Custom API marker idempotent | Run adopt twice against the Custom API assembly. |
Second run does not add a duplicate [CustomApi] (reported as already present / skipped). No further file edits. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| STP-01 | Step name format | Register a step and inspect the name in PRT. | Name is TypeFullName | entity | message | StageName | sync/async. No [dvx] prefix. No integer stage. |
| STP-02 | Stage displayed as text | Use each of the three stages (10, 20, 40). | Step names end with | PreValidation | ..., | PreOperation | ..., | PostOperation | ... respectively. |
| STP-03 | Async mode in step name | Set Async = true. Run sync. |
Step name ends with | async. |
| STP-04 | Sync mode in step name (default) | Omit Async (defaults to false). Run sync. |
Step name ends with | sync. |
| STP-05 | ExecutionOrder / rank |
Set ExecutionOrder = 3. Run sync. Inspect step in PRT. |
Step rank field is 3. |
| STP-06 | Description |
Set Description = "My test step". Run sync. |
Step description field shows "My test step". |
| STP-07 | FilteringAttributes populated |
Set FilteringAttributes = new[] { "name", "telephone1" }. Run sync for an Update step. |
Step filteringattributes field contains "name,telephone1". |
| STP-08 | FilteringAttributes empty (default) |
Omit FilteringAttributes. Run sync. |
Step filteringattributes field is null/empty. Step fires on any field change. |
| STP-09 | FilteringAttributes cleared |
Previously set FilteringAttributes; remove them in code and re-sync. |
Step filteringattributes is cleared. |
| STP-10 | Entity and message lowercased in name | Use Entity = "Account" (mixed case) and Message = "UPDATE" (upper). |
Step name contains | account | update |. |
| STP-11 | Configuration (unsecure) |
Set Configuration = "<x/>". Run sync. Inspect step in PRT. |
Step Unsecure Configuration field shows <x/>. Passed to plugin constructor's first parameter. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| IMG-01 | Pre-image registered | Set UsePreImage = true on a PostOperation step. Run sync. |
One sdkmessageprocessingstepimage record created with imagetype=0 (Pre), entityalias="PreImage". |
| IMG-02 | Pre-image with specific attributes | Set PreImageAttributes = new[] { "name", "telephone1" }. Run sync. |
Pre-image attributes field contains "name,telephone1". |
| IMG-03 | Pre-image with no attributes (all fields) | Set UsePreImage = true but omit PreImageAttributes. Run sync. |
Pre-image attributes field is null/empty (all attributes included). |
| IMG-04 | Post-image on PostOperation | Set UsePostImage = true on a Stage.PostOperation step. Run sync. |
One post-image record created with imagetype=1, entityalias="PostImage". |
| IMG-05 | Post-image on non-PostOperation | Set UsePostImage = true on a Stage.PreOperation step. Run sync. |
Warning logged: "PostImage … will be skipped — only valid on PostOperation." No image record created. Exit 0. |
| IMG-06 | Pre- and post-image both | Set both UsePreImage = true and UsePostImage = true on a PostOperation step. |
Two image records created — one Pre, one Post. |
| IMG-07 | Image updated after attributes change | Change PreImageAttributes and re-run sync. |
Existing image record updated. Image GUID unchanged. New attributes reflected in Dataverse. |
| IMG-08 | Image removed when flag cleared | Set UsePreImage = true, sync. Then set UsePreImage = false, sync again. |
Pre-image record deleted. Step record itself is updated (not recreated). |
| IMG-09 | Image alias reflects PreImageAlias / PostImageAlias |
Register steps with default and custom aliases. Inspect in PRT. | entityalias equals the attribute's PreImageAlias / PostImageAlias; it defaults to "PreImage" / "PostImage" only when unspecified (custom aliases are preserved — consistent with ADO-05). |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| IMP-01 | Neither RunAsSystem nor RunAsUser (default) | Omit both properties. Run sync. |
impersonatinguserid field on step is null. Step runs in calling user's context. |
| IMP-02 | RunAsSystem = true |
Set RunAsSystem = true. Run sync. |
impersonatinguserid is set to the GUID of the SYSTEM user (fetched from Dataverse). Step runs as SYSTEM user. Verify in PRT. |
| IMP-03 | RunAsUser with valid GUID |
Set RunAsUser = "<valid-user-guid>". Run sync. |
impersonatinguserid is an EntityReference to that user's GUID. Verify in PRT. |
| IMP-04 | RunAsUser with invalid string |
Set RunAsUser = "not-a-guid". Run sync. |
Exit 1. Error message includes "not a valid GUID" and the provided value. |
| IMP-05 | Both RunAsSystem and RunAsUser set |
Set both RunAsSystem = true and RunAsUser = "<guid>". Run sync. |
Exit 1. Error message includes "cannot both be set". |
| IMP-06 | RunAsSystem cleared |
Previously synced with RunAsSystem = true. Remove RunAsSystem from attribute. Re-run sync. |
impersonatinguserid cleared on existing step. Step reverts to calling user context. |
| IMP-07 | RunAsUser changed to different user |
Update RunAsUser to a different valid GUID. Re-run sync. |
Step updated in-place (same step GUID). impersonatinguserid now reflects the new user. |
| IMP-08 | RunAsUser cleared |
Previously synced with RunAsUser = "<guid>". Remove RunAsUser from attribute. Re-run sync. |
impersonatinguserid cleared on existing step. Step reverts to calling user context. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| DRY-01 | sync --dry-run reports correct counts |
Run sync --dry-run when steps would be created. |
Output shows Created: N (dry run). No steps or images created in Dataverse. |
| DRY-02 | sync --dry-run reports updates |
Pre-existing step with changed property. Run sync --dry-run. |
Output shows Updated: N (dry run). No Update calls made in Dataverse. |
| DRY-03 | sync --dry-run reports deletes |
Remove a [PluginStep] from code. Run sync --dry-run. |
Output shows Deleted: N (dry run). Step still exists in Dataverse after the run. |
| DRY-04 | register --dry-run |
Run register --dry-run. |
Same behaviour as SYN dry-run tests; step sync runs read-only. |
| DRY-05 | Dry-run + verbose together | Run sync --dry-run --verbose. |
Verbose output is printed (reflection details, step names). No writes to Dataverse. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| VRB-01 | Upload detail printed | Run sync --verbose or deploy --verbose. |
The Uploading <N> KB to pluginpackage {id} detail line is printed alongside the Uploading package content... sub-step. |
| VRB-02 | Reflection details shown during discovery | Run sync --verbose. |
For each IPlugin type, its name and [PluginStep] arguments are printed, including field name, value, and CLR type. |
| VRB-03 | Inner exception chain shown on error | Trigger a Dataverse SDK error with --verbose. |
Inner exception messages are printed with ↳ [ExceptionType] indentation. |
| VRB-04 | No verbose output without flag | Run sync without --verbose. |
No upload detail lines, no reflection details printed. Only step/success summary lines. |
| ID | Description | Steps | Expected result |
|---|---|---|---|
| ERR-01 | Exit 0 on full success | Run sync with all steps successfully created/updated. |
Process exits with code 0. Verify with echo $LASTEXITCODE (PS) or echo $? (bash). |
| ERR-02 | Exit 1 on fatal error | Provide an invalid config path. | Process exits with code 1. |
| ERR-03 | Exit 2 on partial step failure | Simulate by having one step fail while others succeed (e.g. invalid entity name causing a Dataverse error). | Process exits with code 2. Created/Updated count reflects successful steps. Errors listed in output. |
| ERR-04 | Missing required option --env |
Run sync without --env. |
System.CommandLine prints usage help and exits non-zero. No Dataverse connection attempted. |
| ERR-05 | Missing required option --project (sync/deploy) |
Run sync without --project. |
Usage help printed. No Dataverse connection attempted. |
| ERR-06 | Dataverse auth failure | Use an incorrect clientId or clientSecret. |
Exit 1. Error message references the authentication failure. No step operations attempted. |
| ERR-07 | Error message always printed without verbose | Trigger any error without --verbose. |
Error: <message> is printed to the console. No stack trace or inner exceptions shown. |
| ERR-08 | Stack trace / inner exceptions only with verbose | Same error with --verbose. |
Inner exception chain printed below the primary error message. |