API: names the forms require may be blank, and renaming a company onto a taken name answers 500 #438
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#438
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, comparing the API's input schemas with the forms that write the same records, and sending blank and clashing names through the test client.Related to #328.
Checked again on 2026-10-01 against
mainat0a5cad587: still present. The schemas,get_or_create_companyandpatch_companyhave not moved; the constraintunique_company_name_per_owneris now atjobs/models.py:313and the form's check atjobs/forms.py:418-422.The API's input schemas give the required names a maximum length but no minimum, and nothing strips them before the check, although every form requires them. So the API makes companies, listings, applications, contacts, reminders and letters whose name is empty or only spaces.
get_or_create_companystrips a blank company name to''and then matches or makes a company called'', so every later call with a blankcompany_namejoins that one nameless company. Renaming a company throughPATCHskips the other check the company form makes, that no other company of the owner has the name.1. Blank names are accepted
api/schemas.py:101(CompanyIn.name),:120(CompanyPatch.name),:132(ContactIn.name),:265and:271(ListingIn.company_name,title, which the one-step application schema inherits),:513(ReminderIn.summary),:531(ReminderPatch.summary),:577(LetterIn.name);applications/services.py:319-326,get_or_create_company.POST /api/v1/companieswithname: " "makes a company named'';POST /api/v1/listingswithcompany_name: " "andtitle: ""makes an untitled listing at that company, and a second blankcompany_namelands on the same one;POST /api/v1/applicationswith both blank does the same;POST /api/v1/companies/{id}/contactswithname: "",POST /api/v1/reminderswithsummary: ""andPOST /api/v1/letterswithname: ""all succeed;PATCH /api/v1/companies/{id}withname: " "stores the two spaces.2. A rename onto a taken name answers 500, or makes a duplicate
api/routers/companies.py:89-97,patch_company; the constraintunique_company_name_per_owneratjobs/models.py:310; the form's check atjobs/forms.py:417-421,CompanyForm.clean_name.OtherandMine,PATCH /api/v1/companies/{Mine}withname: "Other"raisesIntegrityErrorand answers Django's HTML 500 page; withname: "other"it succeeds and leaves two companies that the rest of Postulo, which matches names case-insensitively (get_or_create_company,CompanyForm), treats as one.POST /api/v1/companies("no second Acme") exists to prevent.Fix
Declare one stripped, non-empty string type in
api/schemas.py, for exampleAnnotated[str, StringConstraints(strip_whitespace=True, min_length=1)], and use it for the fields listed in point 1, keeping their maximum lengths; the patch schemas take the same type as| None. Inpatch_company, refuse a name another of the owner's companies already has, case-insensitively, with a 422, asCompanyForm.clean_namedoes.Done when
tests/test_api.pysends each call in point 1 with a blank or all-space name and asserts a 422 problem document naming the field, and no row made or changed.tests/test_api.pyrenames a company to another company's name, in the same and in a different case, and asserts a 422 with both companies unchanged.