This document describes the testing strategy for the certified-assets project and provides a PR-level checklist for implementation.
- No BATS. All tests use native Rust test infrastructure (
#[test],cargo test). - Three layers: canister unit tests, plugin unit tests, and full E2E tests via the
icpCLI. - Scope: Proposal/governance workflows are out of scope. The
icpCLI with the sync plugin replaces bothicx-assetanddfx deployfrom the old SDK project.
Location: ic-certified-assets/src/tests.rs
Run: cargo test -p ic-certified-assets
Status: Comprehensive (5,277 lines). No new tests planned for this layer.
This test suite covers all canister behaviors using a mock system context (no live replica needed):
| Area | Coverage |
|---|---|
| Batch API | create_batch, commit_batch, drop_batch; atomicity; batch timeout; batch ID persistence across upgrades |
| Asset serving | Encoding selection (identity, gzip, brotli); Accept-Encoding negotiation; correct response body |
| SPA fallback | index.html served for missing paths when aliasing is enabled |
| Stable state | Upgrade/downgrade roundtrips; state survives canister upgrade |
| Streaming | Chunked delivery for large assets |
| HTTP semantics | Custom headers; Cache-Control / max-age; ETags; Content-Integrity |
| Aliases | Enable/disable aliasing behavior |
| IC environment | Root key encoding; public environment cookie |
| Certification | ic_certification tree insertions/deletions; V2 certification correctness |
| Permissions | grant_permission, revoke_permission, list_permitted |
Location: Inline #[cfg(test)] modules in each source file.
Run: cargo test -p assets-sync
All sync business logic lives in the assets-sync library crate, which has no WIT or WASI dependencies and compiles natively. The plugin crate itself contains only the WasiCall transport wrapper and has no testable logic of its own.
Tests for scan() and the private walk() function using tempfile fixtures:
| Test | Asserts |
|---|---|
| Single file | Key is /<filename> with leading slash |
| Nested directory | Recursive walk; key is /<subdir>/<file> |
| Dotfile skipped | .hidden and .gitignore do not appear in results |
| Empty directory | Returns empty Vec |
| Duplicate key across two source dirs | Returns Err with the offending key named |
| Multiple source dirs | Files from both dirs merged into one result |
| Symlink skipped | All symlinks (to files or directories) are excluded from results |
Tests for encoders_for(), Content::load(), Content::encode(), Content::sha256():
| Test | Asserts |
|---|---|
text/html → encoder list |
Returns [Identity, Gzip] |
text/css → encoder list |
Returns [Identity, Gzip] |
application/javascript → encoder list |
Returns [Identity, Gzip] |
text/javascript → encoder list |
Returns [Identity, Gzip] |
image/png → encoder list |
Returns [Identity] only |
application/wasm → encoder list |
Returns [Identity] only |
| Unknown extension → encoder list | Falls back to APPLICATION_OCTET_STREAM; returns [Identity] |
encode(Identity) |
Output data equals input data |
encode(Gzip) |
Output is valid gzip; decompressed equals input |
encode(Brotli) |
Output is valid brotli; decompressed equals input |
sha256() |
Same content produces same digest; different content produces different digest |
Content::load() |
Reads file bytes; infers MIME from extension |
build_operations is a pure function: it takes project_assets and canister_assets maps and returns a Vec<BatchOperationKind>. It is tested inline via #[cfg(test)] without any canister calls. Because assets-sync has no WIT dependency, these tests compile and run natively with no extra stubs needed.
| Test | Asserts |
|---|---|
| New asset (not on canister) | Emits CreateAsset + SetAssetContent ops |
| Unchanged asset (SHA256 matches) | already_in_place = true; emits no ops for that encoding |
| Updated asset (SHA256 differs) | Emits SetAssetContent; no CreateAsset |
| Deleted asset (on canister, not in project) | Emits DeleteAsset |
| Content-type mismatch (same key, MIME changed) | Emits DeleteAsset + CreateAsset + SetAssetContent |
Stale encoding on canister (e.g. gzip present but project only has identity) |
Emits UnsetAssetContent for the stale encoding |
| New encoding added (e.g. file now compressible) | Emits SetAssetContent for the new encoding |
| Empty project, non-empty canister | All canister assets deleted |
| Everything in sync | Returns empty Vec; commit_batch not called |
| Gzip skipped when compressed ≥ original size | No SetAssetContent op for gzip encoding |
Location: New workspace member crate at e2e/
Run: cargo test -p e2e
These tests verify the complete pipeline: plugin.wasm built → loaded by icp → assets synced to a live canister.
The e2e/ crate uses:
| Crate | Role |
|---|---|
assert_cmd |
Invokes icp as a subprocess and asserts exit code / stdout |
tempfile |
Provides throwaway asset directories and icp.yaml configs |
Setup per test:
- Create a temporary project directory containing:
icp.yamlreferencing the workspace-builtcanister.wasmandplugin.wasm.- An asset directory populated either programmatically by the test or from committed test fixtures.
- Start a local network with
icp network start -d; shut it down withicp network stopin test cleanup. - Run
icp deployto install the canister WASM and execute the plugin sync step. - Verify the resulting canister state with
icp canister call(Candid text for both arguments and return values).
| Test | Scenario | Asserts |
|---|---|---|
| Initial sync | Empty canister, one HTML + one PNG file | Both keys present; content types correct |
| No-op sync | Run sync a second time without changes | Plugin logs "already up to date"; canister state unchanged |
| Content update | Modify HTML file content; re-sync | SHA256 on canister updated; other assets unchanged |
| Asset deletion | Remove a file from the local directory; re-sync | Key deleted from canister; remaining assets intact |
| Multi-directory | Two source dirs with non-overlapping files | All files from both dirs uploaded; keys namespaced correctly |
| Test | File type | Asserts |
|---|---|---|
| Text file gets gzip | .html / .css / .js |
Canister holds both identity and gzip encodings |
| Binary file identity-only | .png / .wasm |
Canister holds identity encoding only; no gzip |
| Gzip skipped when not smaller | Tiny text file where gzip output ≥ original | Only identity encoding stored |
| Test | Scenario | Asserts |
|---|---|---|
| Multi-chunk upload | File > 1.9 MB | Plugin splits into multiple chunks; canister reconstructs correctly; SHA256 verified end-to-end |
| Test | Scenario | Asserts |
|---|---|---|
| Pagination | Sync > 100 assets | list_assets pagination loop retrieves all assets; count matches local files |
| Test | Scenario | Asserts |
|---|---|---|
| Unauthorized identity | Sync with an identity that has no Commit permission |
icp exits with non-zero; error message mentions permission |
| Proxy mode: permission grant | Sync in proxy mode where identity lacks Commit |
Plugin grants permission via proxy; sync succeeds |
| Proxy mode: already permitted | Identity already has Commit |
Grant step skipped (log message confirms); sync succeeds |
| Old SDK test | Replaced by |
|---|---|
ic-certified-assets/src/tests.rs |
ic-certified-assets/src/tests.rs (already ported and expanded) |
ic-asset/src/sync.rs unit tests |
assets-sync unit tests — scan.rs (Layer 2a) |
ic-asset/src/batch_upload/operations.rs unit tests |
assets-sync unit tests — sync.rs::build_operations (Layer 2c) |
ic-asset/src/asset/config.rs unit tests |
Not yet in scope (plugin has no .ic-assets.json5 support yet) |
icx-asset.bash (BATS) |
E2E Layer 3 — basic sync, encoding, chunking, pagination |
assetscanister.bash (BATS) — canister API behaviors |
Covered by existing Layer 1 unit tests |
assetscanister.bash — permission checks |
E2E Layer 3 — authorization tests |
frontend.bash (BATS) |
Out of scope (dfx deploy UI/UX not applicable) |
| Playwright browser tests | Out of scope |
| Proposal / governance tests | Out of scope |
-
scan.rsunit tests
Inline#[cfg(test)]module inassets-sync/src/scan.rs. Usestempfilefor fixtures.
Covers: single file, nested dirs, dotfile skip, empty dir, duplicate key error, multiple source dirs. -
content.rsunit tests
Add inline#[cfg(test)]module toassets-sync/src/content.rs.
Covers:encoders_forby MIME type, gzip/brotli round-trips, SHA256 determinism, identity passthrough. -
sync.rs::build_operationsunit tests
Inline#[cfg(test)]module inassets-sync/src/sync.rs. No WIT constraint applies sinceassets-synchas no WASI dependency.
Covers: create, no-op, update, delete, type-mismatch recreate, stale encoding unset, new encoding set, gzip-not-smaller skip, empty-project delete-all, everything-in-sync.
-
E2E infrastructure
Adde2e/crate to workspace. Wire upassert_cmdandtempfile. Add a skeleton test and a new CI job (cargo test -p e2e). Document theicp network start -dlifecycle and teardown pattern, and the convention for parsingicp canister calloutput in assertions. -
Basic sync E2E tests
Covers: initial sync, no-op sync, content update, asset deletion, multi-directory sync. -
Encoding policy E2E tests
Covers: text gets gzip, binary identity-only, gzip skipped when not smaller. -
Chunking and pagination E2E tests
Covers: multi-chunk upload for files > 1.9 MB, and list pagination with > 100 assets. -
Authorization E2E tests
Covers: unauthorized sync rejects, proxy mode grants permission, proxy mode skips redundant grant.