Make the API's optional file extension actually optional - #811
Merged
Conversation
`accountFile/upload` declares `extension` optional in its own help. Omitting it
answered HTTP 500:
{"error":{"message":"Integrity constraint",
"detail":"SQLSTATE[23000]: ... Column 'extension' cannot be null"}}
`AccountFile.extension` is `varchar(10) NOT NULL` with no default, the
repository does not filter nulls out of the insert, and a null model property
overrides a column's default — so using the endpoint exactly as documented
handed an API client a raw SQLSTATE.
Two things kept it invisible. Every one of the sixteen existing tests passes an
`extension`, so the optional path was never exercised. And
`ApiHelpMatchesControllersTest` passes, correctly: the help and the controller
agree with each other about the parameter being optional. They both disagree
with the database.
The extension is now derived from the file name when it is not given, which is
what the web upload has always done (`mb_strtoupper(pathinfo(...))`), so the
same file arriving by either route is recorded the same way. A name carrying no
extension stores an empty string — a fact about the file — rather than a null
the database refuses.
The new tests read the stored row rather than the upload's own response, which
echoes back only the id and the name and so cannot show what was persisted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
accountFile/uploaddeclaresextensionoptional in its own help. Omitting it answered HTTP 500:{"error":{"message":"Integrity constraint", "detail":"SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'extension' cannot be null"}}AccountFile.extensionisvarchar(10) NOT NULLwith no default, the repository does not filter nulls out of the insert, and a null model property overrides a column's default — so using the endpoint exactly as documented handed an API client an internal database error.Why nothing caught it
Two independent reasons, both worth knowing:
extension. The optional path had never been exercised, which is the pattern that keeps producing findings here: prefer an untested surface over an uncovered branch in a tested one.ApiHelpMatchesControllersTestpasses, and is right to. It checks that each declared parameter is one its controller reads, with the same required flag — and the help and the controller do agree with each other. They both disagree with the database, which that test has no way to see.The fix
The extension is derived from the file name when the caller does not give one, which is what the web upload has always done (
mb_strtoupper(pathinfo($fileName, PATHINFO_EXTENSION))). The same file arriving by either route is now recorded the same way, instead of one route storingTXTand the other refusing the insert.A name carrying no extension stores an empty string. That is a fact about the file — plenty of real attachments have none — where null is an insert the server rejects.
Tests
Three cases: no extension given (taken from the name), a name with no extension at all (stored empty), and an extension the caller does give (kept, and normalised the way the web normalises it).
Each reads the stored row rather than the upload's response, which echoes back only the id and the name and so cannot show what was persisted — the thing that was actually wrong.
Reverting the fix fails all three, two of them with the same HTTP 500.
3965 unit + 961 integration pass; PHPStan level 6 on
srcand PHPCS clean.