Files
jobtrackingapp/docs/audits/evidence/runtime-evidence.md
T

5.6 KiB

Disposable runtime evidence

Captured: 2026-08-02

Environment: local Development configuration, compiled Release API, Next development server, disposable SQLite data root under the current user's temporary directory. No production service, real mailbox, paid AI provider, or personal data was used.

Default-provider endpoint results

Two synthetic local users were registered. User A owned one company, one job (Syntetisk utvikler æøå), one correspondence record, two CV variants, one career profile, and one text attachment. User B had no data.

Request User Result Evidence
GET /api/jobapplications?page=1&pageSize=20 B 200, 0 items Empty-account isolation works.
GET /api/companies B 200, 0 items Empty-account isolation works.
GET /api/correspondence?jobApplicationId=1 B 200, 0 items User A's message was not disclosed.
GET /api/careerprofile B 404 User A's profile was not disclosed.
GET /api/jobapplications/1 A / B 200 / 404 Job ownership enforced.
GET /api/attachments/1 A / B 200 / 404 Attachment row and file ownership enforced.
GET /api/cv/variants A and empty B 500 EF SQLite cannot translate the DateTimeOffset ordering.
GET /api/jobapplications/1/workspace A 500 Same provider-translation family, reached on the core workspace.
GET /api/jobapplications/1/ai/history A 500 Same provider-translation family.
GET /api/ai/usage A 500 Same provider-translation family.
GET /api/jobapplications/1/timeline A and B 500 Ambiguous endpoint selection occurs before ownership evaluation.
GET /api/jobapplications/1/interview-prep A and B 500 Ambiguous endpoint selection occurs before ownership evaluation.
GET /api/jobapplications/1/checklist A 200 Sibling workspace endpoint works.
GET /api/jobapplications/1/analysis A 200 Sibling intelligence endpoint works.
GET /api/jobapplications/1/match A 200 Sibling intelligence endpoint works.

The application returned generic 500 problem responses; no secret-bearing exception details were exposed to the client.

Background-service reproduction

The synthetic job was set to Applied, dated 40 days earlier than the audit, and had Tags and ShortSummary set to null. After a controlled API restart and more than the documented initial delays:

  • status remained Applied, although the default rules threshold should have marked it ghosted;
  • Tags remained null despite deterministic SkillTagger input containing C#;
  • ShortSummary remained null.

This reproduces the deny-on-null tenant filter in a background scope with no HTTP user. The same query structure exists in rules, follow-up reminders, enrichment, and daily export. The queued CV processor is the counterexample: it deliberately uses IgnoreQueryFilters() for cross-tenant worker reads.

Authentication lifecycle reproductions

Logout

  1. Logged in as User A and copied the issued synthetic cookie jar.
  2. Posted logout with the CSRF cookie/header pair.
  3. The browser jar received cookie deletion and subsequently got 401.
  4. The copied pre-logout cookie still received 200 from /api/auth/me.

Result: logout clears client cookies but does not revoke the server-side UserSession.

Email verification

A separate disposable API started with Auth:RequireEmailVerification=true, registration enabled, and email delivery disabled.

Check Result
Register synthetic account 200
Stored EmailConfirmed 0
Immediate /api/auth/me with registration-issued session 200

The same synthetic row was then marked confirmed solely to isolate the email-change path. A normal login followed by PUT /api/auth/profile changed the email, returned 204, and left EmailConfirmed=1. No verification challenge was required for the new address.

Backup and restoration rehearsal

The built-in SQLite backup runner created jobtracker_backup_20260802_184234.db from the disposable database. A read-only copied snapshot passed PRAGMA integrity_check and matched the current database:

Dataset Current Restored copy
Users 2 2
Jobs 1 1
Companies 1 1
Attachment rows 1 1
CV variants 2 2

A second isolated API was started from the restored database plus separately copied Attachments/ and keys/. /health returned 200; synthetic login, job retrieval, and attachment download all returned 200.

Conclusion: SQLite database snapshotting works. Complete recovery additionally requires file storage, the Data Protection key ring, deployment secrets/configuration, and a documented restore procedure. MariaDB/MySQL backup is intentionally unsupported by the in-process runner and was not available for rehearsal.

Safe local performance samples

Release API, disposable SQLite database with one job, ten requests after warm-up:

Endpoint Mean p95 Response bytes
/health 11.3 ms 43.9 ms 35
/api/jobapplications?page=1&pageSize=20 17.8 ms 111.3 ms 1,547
/api/companies 6.5 ms 33.3 ms 255

The static export contained 49 JavaScript chunks totalling 2,762,618 uncompressed bytes and one 293-byte CSS file. This aggregate is not an initial-page transfer measurement because Next.js loads route chunks selectively.

No load test was performed. Results do not establish production capacity.

Cleanup

Listeners on audit ports 3001, 5402, 5403, and 5404 were resolved to their exact audit-owned command lines and stopped. Temporary evidence remains under the disposable local data root; no repository source/configuration was changed.