Bite 32.7 — Tenant People CSV Export and Test‐to‐Production Portability - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki

Bite 32.7 — Tenant People CSV Export and Test-to-Production Portability

Manual Verification Checklist Before Promotion

Branch: 922-bite-327-tenant-people-csv-export-and-test-to-production-portability

Most recent supplied HEAD used to verify the current scripts:
691f8167b41a588914bf3013230b9757794b54ee

Automated prerequisite: all LOCAL and CI tests pass after the Bite 32.7 portability repairs.


1. Purpose of this manual suite

Bite 32.7 provides a controlled way for a Tenant Administrator to export the selected Tenant's People into the canonical CSV format consumed by import-people.

The manual suite must prove all of the following:

  • a Tenant Administrator can export People through the supported UI;
  • the export is scoped to the selected Tenant;
  • the CSV uses the canonical importer header;
  • the export does not contain environment-specific Person, Membership, Tenant, Actor, Account, or source-Tenant reference-data IDs;
  • the legacy-named statusId CSV column contains a stable status code such as ACTIVE, rather than a source-Tenant reference-data ID;
  • import-people can resolve that stable status code against a different target Tenant;
  • valid source People survive an export → target-Tenant importer dry-run;
  • the Brazilian demo fixture is not used for importer-validity testing because its CPF values deliberately have invalid check digits;
  • TENANT_VIEWER / Entity Executive (Read Only) does not receive the bulk export capability.

This suite is intentionally UI-driven wherever the behavior is visible through the application. Shell/SQLite commands are used only to prepare isolated databases and to perform the importer portability proof.


2. Important CPF rule

Do not use the Brazilian demo Tenant Mineração Serra Dourada — DEMO as the source of the importer portability test.

Its demo CPF values intentionally have invalid check digits.

That fixture may still be used for:

  • pt-BR presentation testing;
  • confirming the Export CSV button is translated;
  • CSV download behavior;
  • CSV quoting/structure;
  • authorization behavior.

It must not be used for the export → importer-validity round trip.

For the portability test in this checklist, use a dedicated Bite 32.7 LOCAL database seeded with:

scripts/seed-bite30g-final-manual-testdata.py

That fixture uses the canonical post-30K Person/Membership schema and generates deterministic CPFs with valid check digits.


3. Record the exact code under test

From the ERS repository root:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems

git rev-parse --abbrev-ref HEAD
git rev-parse HEAD
git status --short

Record:

Branch:
HEAD SHA:
Working tree status:

Expected:

  • branch is 922-bite-327-tenant-people-csv-export-and-test-to-production-portability;
  • working tree contains only changes you intentionally expect;
  • this exact SHA is the one being considered for promotion.

Promotion blocker: testing a different branch/SHA than the one intended for promotion.


4. Create a fresh source database containing valid CPFs

Stop any running backend first.

From the repository root:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems

export BITE327_SOURCE_DB="$PWD/backend/data/bite327-source-valid-cpf.db"

rm -f \
  "$BITE327_SOURCE_DB" \
  "$BITE327_SOURCE_DB-wal" \
  "$BITE327_SOURCE_DB-shm" \
  "$BITE327_SOURCE_DB-journal"

Apply the complete current migration chain:

DB_PATH="$BITE327_SOURCE_DB" ./scripts/db-migrate.sh

Expected final result includes:

✅ Migrations applied.

Verify database integrity:

sqlite3 "$BITE327_SOURCE_DB" "PRAGMA integrity_check;"

Expected:

ok

Now seed the canonical valid-CPF fixture:

python3 scripts/seed-bite30g-final-manual-testdata.py \
  --db-path "$BITE327_SOURCE_DB" \
  --batch bite327 \
  --no-backup

Expected output includes:

Bite 30G final manual fixtures created successfully

and prints:

  • database path;
  • fixture manifest path;
  • Tenant A;
  • Tenant B;
  • Tenant A Administrator login;
  • Tenant B Administrator login;
  • Person/Tenant account password.

For batch bite327, the expected Tenant Administrator credentials are:

Tenant A Administrator:
  Login:    [email protected]
  Password: Manual-30C-Password!

Tenant B Administrator:
  Login:    [email protected]
  Password: Manual-30C-Password!

Tenant mapping:

Tenant A:
  Name: Byte 28A Manual Test
  ID:   manual30g-bite327-tenant-a

Tenant B:
  ID: default

The seeder output is the authority if any displayed value differs.


5. Verify that the source fixture really contains valid-CPF People

Run:

sqlite3 -header -column "$BITE327_SOURCE_DB" "
SELECT
    gp.first_name,
    gp.last_name,
    gp.cpf,
    gp.email,
    m.tenant_id,
    rd.code AS status_code,
    rd.id   AS source_status_id
FROM global_people gp
JOIN person_tenant_memberships m
  ON m.person_id = gp.id
LEFT JOIN reference_data rd
  ON rd.id = m.status_id
WHERE gp.id LIKE 'manual30g-bite327-%'
ORDER BY m.tenant_id, gp.last_name, gp.first_name;
"

Record at least:

One Tenant A Person email:
One Tenant A CPF:
One Tenant A source_status_id:

One Tenant B-only Person email:

Expected:

  • populated 11-digit CPF values;
  • status codes such as ACTIVE;
  • Tenant A source status IDs are Tenant/source-specific identifiers, not simply the text ACTIVE.

Do not manually edit the CPF values.


6. Start LOCAL against the Bite 32.7 source database

Terminal 1:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems

ERS_DATABASE_PATH="$BITE327_SOURCE_DB" \
make local-backend

Important:

  • do not add ERS_RESET_DATABASE=true;
  • the database has already been migrated and seeded;
  • resetting it here would destroy the fixture you just prepared.

Confirm the backend stays running and reports no migration/SQLite error.

Terminal 2:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems

make local-frontend

Open the normal LOCAL application in the browser.


7. Test 01 — Tenant Administrator can export the selected Tenant

Identity

Sign in as:

[email protected]

Password:

Manual-30C-Password!

Required context

Select:

Byte 28A Manual Test

Do not use default for this test.

Using Tenant A as the source is useful because the export will later be dry-run imported into the different target Tenant default.

Procedure

  1. Sign in through the normal ERS login UI.
  2. Confirm the selected Tenant is Byte 28A Manual Test.
  3. Navigate through the normal application navigation to People.
  4. Wait for the People list to finish loading.
  5. Record at least two displayed People.
  6. Confirm the list includes the Tenant A Person/email recorded in Section 5.
  7. Click Export CSV.
  8. Confirm the browser downloads a CSV file.
  9. Do not edit the downloaded file.
  10. Rename it only if necessary for clarity:
mv ~/Downloads/ers-people-export.csv \
   ~/Downloads/bite327-tenant-a-people-export.csv

Use the browser's actual downloaded filename if it differs.

Expected

  • export action is available to the Tenant Administrator;
  • clicking it downloads a CSV;
  • the People displayed before export belong to the selected Tenant;
  • there is no application error;
  • there is no unexpected 401, 403, or 500.

PASS

PASS if the Tenant Administrator can export the selected Tenant's People successfully.

Promotion blockers

  • no Export CSV action for a legitimate Tenant Administrator;
  • download fails;
  • unexpected server error;
  • export operation changes the selected Tenant.

8. Test 02 — Canonical CSV contract and portable status codes

Set a shell variable to the downloaded file:

export BITE327_EXPORT="$HOME/Downloads/ers-people-export.csv"

Display the first few lines:

sed -n '1,6p' "$BITE327_EXPORT"

Required canonical header

The first line must be exactly:

firstName,lastName,nickname,cpf,rg,cellular,email,statusId,notes,street1,street2,city,state,cep,country,bankName,bankNumber,checkingAccount,pixKey,emergencyName,emergencyCellular,emergencyEmail

Verify it exactly:

head -1 "$BITE327_EXPORT"

Verify portable status values

Inspect column 8 (statusId):

python3 - "$BITE327_EXPORT" <<'PY'
import csv
import sys

path = sys.argv[1]

with open(path, newline="", encoding="utf-8-sig") as f:
    rows = list(csv.DictReader(f))

print("Rows:", len(rows))
print("statusId values:", sorted({r["statusId"] for r in rows}))
for r in rows:
    print(r["email"], "->", r["statusId"])
PY

Expected values are stable codes such as:

ACTIVE
INACTIVE
DISCONTINUED

The values must not look like source-Tenant reference-data IDs.

Explicit source-status-ID leak check

Copy the Tenant A source_status_id recorded in Section 5:

export SOURCE_STATUS_ID='PASTE-THE-RECORDED-SOURCE-STATUS-ID-HERE'

Then:

if grep -Fq "$SOURCE_STATUS_ID" "$BITE327_EXPORT"; then
  echo "FAIL: source-Tenant status ID leaked into export"
else
  echo "PASS: source-Tenant status ID is absent"
fi

Expected:

PASS: source-Tenant status ID is absent

Environment-specific ID spot check

The CSV must not expose:

  • Tenant ID;
  • Person database ID;
  • Membership ID;
  • Actor ID;
  • Authentication Account ID.

At minimum, confirm the source Tenant ID does not occur:

if grep -Fq "manual30g-bite327-tenant-a" "$BITE327_EXPORT"; then
  echo "FAIL: source Tenant ID leaked into export"
else
  echo "PASS: source Tenant ID is absent"
fi

PASS

PASS if:

  • the header is exactly canonical;
  • every row has a portable statusId value;
  • the source status ID does not occur;
  • the source Tenant ID does not occur.

Promotion blockers

  • raw source status/reference-data ID appears;
  • Tenant ID appears;
  • header differs from importer contract;
  • statusId is blank for a row that has an active source status.

9. Test 03 — Tenant isolation

This test proves Tenant A export does not include Tenant B-only People.

Preparation

From Section 5, use one Person/email known to belong only to Tenant B (default).

Set it:

export TENANT_B_ONLY_EMAIL='PASTE-TENANT-B-ONLY-EMAIL-HERE'

Search the Tenant A export:

if grep -Fiq "$TENANT_B_ONLY_EMAIL" "$BITE327_EXPORT"; then
  echo "FAIL: Tenant B-only Person leaked into Tenant A export"
else
  echo "PASS: Tenant B-only Person is absent"
fi

Expected:

PASS: Tenant B-only Person is absent

Also verify the known Tenant A Person recorded in Section 5 is present:

export TENANT_A_EMAIL='PASTE-TENANT-A-EMAIL-HERE'

grep -Fi "$TENANT_A_EMAIL" "$BITE327_EXPORT"

Expected:

  • one matching CSV row for the Tenant A Person.

PASS

PASS if:

  • known Tenant A Person is present;
  • known Tenant B-only Person is absent.

Promotion blocker

Any Person belonging only to another Tenant appears in the export.


10. Test 04 — Entity Executive / TENANT_VIEWER cannot bulk-export People

This is an authorization test.

The TENANT_VIEWER role is displayed in the UI as:

Entity Executive (Read Only)

Preparation using the supported UI

Remain signed in as the Tenant A Administrator.

  1. Keep Tenant context Byte 28A Manual Test.
  2. Navigate to Administration → Authorization.
  3. Select a fixture Person/Actor in Tenant A that does not already hold another conflicting delegated Tenant role.
  4. Grant:
Entity Executive (Read Only)
  1. Confirm the grant succeeds through the normal UI.
  2. Record the selected Person's login.
  3. Sign out.

If the selected fixture Person already has a conflicting non-baseline Tenant role, choose another eligible Tenant A Person rather than bypassing role-isolation rules.

Procedure

  1. Sign in as the Person just granted Entity Executive (Read Only).
  2. Select Byte 28A Manual Test.
  3. Navigate to People.
  4. Confirm read-only People visibility works.
  5. Inspect the People page for the bulk Export CSV action.
  6. Do not use DevTools to bypass the UI.

Expected

  • Entity Executive can see the read-only People surface according to its role;
  • bulk Export CSV is not available as an actionable Tenant Viewer capability;
  • no accidental export occurs.

PASS

PASS if the read-only role cannot bulk export Tenant People.

Promotion blocker

A TENANT_VIEWER / Entity Executive can download the bulk People CSV.


11. Test 05 — Create a completely separate target database

Stop the source backend before changing databases.

From the repository root:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems

export BITE327_TARGET_DB="$PWD/backend/data/bite327-target-portability.db"

rm -f \
  "$BITE327_TARGET_DB" \
  "$BITE327_TARGET_DB-wal" \
  "$BITE327_TARGET_DB-shm" \
  "$BITE327_TARGET_DB-journal"

Apply the current migration chain:

DB_PATH="$BITE327_TARGET_DB" ./scripts/db-migrate.sh

Verify:

sqlite3 "$BITE327_TARGET_DB" "PRAGMA integrity_check;"

Expected:

ok

Do not seed the Brazilian demo fixture into this target database.

Do not seed the same Bite 30G source People into this target database.

The target should remain a clean current-schema database so the exported People are genuinely new to it.


12. Test 06 — Confirm source and target status IDs are not being carried across

First show the target Tenant's active Person-status reference data:

sqlite3 -header -column "$BITE327_TARGET_DB" "
SELECT
    tenant_id,
    type,
    code,
    id,
    active
FROM reference_data
WHERE tenant_id = 'default'
  AND type = 'person_status'
ORDER BY code;
"

Record:

Target ACTIVE status ID:

Now compare it with the source Tenant A source_status_id recorded earlier.

The purpose is not to require a specific ID format.

The requirement is:

  • CSV contains the stable code, e.g. ACTIVE;
  • importer resolves that code against the target Tenant's own reference data;
  • importer does not depend on the source Tenant's reference-data ID.

13. Test 07 — Export → target-Tenant importer dry-run

From the repository root:

cd backend

Run:

go run ./cmd/import-people \
  -db "$BITE327_TARGET_DB" \
  -file "$BITE327_EXPORT" \
  -tenant-id default \
  -dry-run

Expected

The report must be:

People CSV dry-run report
Rows read:      <N>
Rows validated: <N>
Rows inserted:  0
Errors:         0

where:

Rows validated == Rows read
Rows inserted  == 0
Errors         == 0

There must be no errors such as:

cpf: CPF is invalid

and no error such as:

statusId: Status must be an active person status ID or code for the target Tenant

What this proves

A successful dry-run proves all of these together:

  1. exported source CPFs are importer-valid;
  2. source CSV structure matches the importer contract;
  3. source-Tenant raw status IDs are not required;
  4. stable status codes such as ACTIVE are accepted;
  5. the code resolves against target Tenant default;
  6. all rows survive validation without writing them.

PASS

PASS only if:

Rows validated == Rows read
Errors == 0
Rows inserted == 0

Promotion blockers

Any of:

  • invalid CPF error from this valid-CPF fixture;
  • unresolved statusId;
  • malformed CSV;
  • duplicate/environment-ID dependency;
  • fewer validated rows than rows read;
  • any unexpected importer error.

14. Test 08 — Prove dry-run made no Person inserts

After the successful dry-run:

sqlite3 -header -column "$BITE327_TARGET_DB" "
SELECT COUNT(*) AS imported_source_people
FROM global_people
WHERE email LIKE 'manual30g-bite327.%@example.test';
"

Expected:

imported_source_people
----------------------
0

The exact formatting may differ, but the count must be 0.

PASS

PASS if dry-run validated the rows without inserting source People.

Promotion blocker

Any source fixture Person is inserted during -dry-run.


15. Optional Test 09 — Real import into the disposable target database

This test is optional because the dry-run is the promotion-critical portability proof.

Perform it only against the disposable target database created above.

Before importing, make a disposable backup:

cp "$BITE327_TARGET_DB" "$BITE327_TARGET_DB.before-real-import"

Then:

cd /Users/rodrigosilveira/projects/EnterpriseRemoteSystems/backend

go run ./cmd/import-people \
  -db "$BITE327_TARGET_DB" \
  -file "$BITE327_EXPORT" \
  -tenant-id default

Expected:

Rows read:      <N>
Rows validated: <N>
Rows inserted:  <N>
Errors:         0

with:

Rows read == Rows validated == Rows inserted
Errors == 0

Then verify the imported Memberships resolve to target-Tenant reference data:

sqlite3 -header -column "$BITE327_TARGET_DB" "
SELECT
    gp.first_name,
    gp.last_name,
    gp.email,
    m.tenant_id,
    rd.code AS status_code,
    rd.id   AS target_status_id
FROM global_people gp
JOIN person_tenant_memberships m
  ON m.person_id = gp.id
LEFT JOIN reference_data rd
  ON rd.id = m.status_id
WHERE gp.email LIKE 'manual30g-bite327.%@example.test'
ORDER BY gp.email;
"

Expected:

  • tenant_id = default;
  • status code resolves correctly, normally ACTIVE;
  • target_status_id belongs to default reference data;
  • no source Tenant A status ID is required.

Restore/delete the disposable target DB afterward as desired.


16. Test 10 — Browser and backend sanity check

During the UI portions of Tests 01–04:

  1. Keep browser DevTools Console available.
  2. Review failed Network requests.
  3. Review the backend terminal.

Expected:

  • no unexpected JavaScript exception;
  • no unexpected 500;
  • no unexpected 401/403 for the Tenant Administrator export;
  • expected authorization restrictions for TENANT_VIEWER are clean and do not produce a server error;
  • no panic;
  • no SQLite error.

Browser-extension messages unrelated to ERS are not Bite 32.7 defects.


17. Final database integrity checks

Source:

sqlite3 "$BITE327_SOURCE_DB" "PRAGMA integrity_check;"
sqlite3 "$BITE327_SOURCE_DB" "PRAGMA foreign_key_check;"

Target:

sqlite3 "$BITE327_TARGET_DB" "PRAGMA integrity_check;"
sqlite3 "$BITE327_TARGET_DB" "PRAGMA foreign_key_check;"

Expected:

For each integrity_check:

ok

For each foreign_key_check:

no rows

18. Promotion gate

Bite 32.7 is ready for promotion when all required items below pass:

  • All LOCAL automated tests pass.
  • All CI tests pass.
  • The exact branch/SHA intended for promotion was tested.
  • Fresh source DB was migrated successfully.
  • seed-bite30g-final-manual-testdata.py seeded the valid-CPF fixture successfully.
  • Source DB integrity check is ok.
  • Tenant Administrator can export People through the UI.
  • Export is from the selected Tenant only.
  • Canonical CSV header is exact.
  • Export contains stable status codes such as ACTIVE.
  • Export does not contain the source Tenant's raw status reference-data ID.
  • Export does not contain the source Tenant ID.
  • Tenant B-only Person does not leak into Tenant A export.
  • TENANT_VIEWER / Entity Executive (Read Only) cannot bulk export People.
  • Fresh target DB was migrated successfully.
  • Target DB integrity check is ok.
  • Export → importer dry-run against target Tenant default succeeds.
  • Rows validated == Rows read.
  • Rows inserted == 0 during dry-run.
  • Errors == 0.
  • No CPF validation error occurs for the valid-CPF fixture.
  • No target status-resolution error occurs.
  • Dry-run inserts no source People.
  • Source and target foreign-key checks are clean.
  • No unexpected browser/backend/server errors occur.

19. Promotion-blocking defects

Do not promote Bite 32.7 if any of the following occurs:

  • Tenant Administrator cannot export the selected Tenant's People.
  • Export leaks a Person from another Tenant.
  • Export includes source Tenant ID, Membership ID, Person ID, Actor ID, Account ID, or another environment-specific identity.
  • statusId carries a source-Tenant reference-data ID instead of the stable status code.
  • Exported valid-CPF fixture fails CPF validation.
  • ACTIVE, INACTIVE, or DISCONTINUED cannot be resolved against the target Tenant.
  • Importer dry-run validates fewer rows than it reads.
  • Importer dry-run reports any error.
  • Importer dry-run inserts rows.
  • Entity Executive / TENANT_VIEWER receives bulk export capability.
  • Database integrity or foreign-key checks fail.
  • Any cross-Tenant authorization/data leak is observed.
  • Any unexpected server 500 or panic occurs in the Bite 32.7 workflow.

20. Results to record

Record these values before promotion:

Branch:
HEAD SHA:

Source DB:
Source Tenant:
Source Tenant Administrator login:
Source CSV filename:
Source CSV row count:
Source ACTIVE status ID:

Target DB:
Target Tenant:
Target ACTIVE status ID:

Dry-run rows read:
Dry-run rows validated:
Dry-run rows inserted:
Dry-run errors:

Tenant isolation result:
TENANT_VIEWER export authorization result:

Source PRAGMA integrity_check:
Source PRAGMA foreign_key_check:

Target PRAGMA integrity_check:
Target PRAGMA foreign_key_check:

Overall result: PASS / FAIL

Glossary impact

none