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
statusIdCSV column contains a stable status code such asACTIVE, rather than a source-Tenant reference-data ID; import-peoplecan 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
- Sign in through the normal ERS login UI.
- Confirm the selected Tenant is Byte 28A Manual Test.
- Navigate through the normal application navigation to People.
- Wait for the People list to finish loading.
- Record at least two displayed People.
- Confirm the list includes the Tenant A Person/email recorded in Section 5.
- Click Export CSV.
- Confirm the browser downloads a CSV file.
- Do not edit the downloaded file.
- 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, or500.
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
statusIdvalue; - 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;
statusIdis 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.
- Keep Tenant context Byte 28A Manual Test.
- Navigate to Administration → Authorization.
- Select a fixture Person/Actor in Tenant A that does not already hold another conflicting delegated Tenant role.
- Grant:
Entity Executive (Read Only)
- Confirm the grant succeeds through the normal UI.
- Record the selected Person's login.
- 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
- Sign in as the Person just granted Entity Executive (Read Only).
- Select Byte 28A Manual Test.
- Navigate to People.
- Confirm read-only People visibility works.
- Inspect the People page for the bulk Export CSV action.
- 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:
- exported source CPFs are importer-valid;
- source CSV structure matches the importer contract;
- source-Tenant raw status IDs are not required;
- stable status codes such as
ACTIVEare accepted; - the code resolves against target Tenant
default; - 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_idbelongs todefaultreference 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:
- Keep browser DevTools Console available.
- Review failed Network requests.
- Review the backend terminal.
Expected:
- no unexpected JavaScript exception;
- no unexpected
500; - no unexpected
401/403for the Tenant Administrator export; - expected authorization restrictions for
TENANT_VIEWERare 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.pyseeded 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
defaultsucceeds. -
Rows validated == Rows read. -
Rows inserted == 0during 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.
statusIdcarries a source-Tenant reference-data ID instead of the stable status code.- Exported valid-CPF fixture fails CPF validation.
ACTIVE,INACTIVE, orDISCONTINUEDcannot 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_VIEWERreceives bulk export capability. - Database integrity or foreign-key checks fail.
- Any cross-Tenant authorization/data leak is observed.
- Any unexpected server
500or 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