* test(api): add regression tests for create-project endpoint
Cover three scenarios:
- project_lead set to the creator's own user_id
- project_lead set to a different workspace member
- project_lead omitted (baseline)
The first two currently fail on preview because of a UUID coercion
bug in ProjectMember.objects.create — see follow-up commit.
* fix(api): pass project_lead_id (not User instance) when creating ProjectMember
The create-project endpoint built a ProjectMember row with
member_id=serializer.instance.project_lead, which resolves to a User
instance via Django's related descriptor instead of a UUID. Django's
UUIDField coercion then fails with AttributeError: 'User' object has
no attribute 'replace', which the generic exception handler converts
to a 400 "Please provide valid detail" — but only after the Project
row was already persisted, leaving an orphaned project without
default states.
Fix:
- Use project_lead_id (FK ID, no descriptor lookup) on both the guard
comparison and the ProjectMember creation.
- Wrap the post-save flow in transaction.atomic() so any future
exception triggers a clean rollback.
- Defer model_activity.delay() with transaction.on_commit() so the
activity log only fires after a successful commit.
- Capture the exception with log_exception() in the generic catch so
future regressions surface in api logs.
Note: a related data integrity issue exists where
ProjectCreateSerializer doesn't create a ProjectIdentifier row
(unlike its frontend counterpart). Out of scope here, will follow
up in a separate PR.
* fix(api): return 500 on unexpected errors and harden project create
Address review feedback from @sriramveeraghanta on PR #8966:
- The catch-all `except Exception` now returns 500 instead of 400.
Reusing the generic 400 response on a server-side crash was the
anti-pattern that hid the original ghost-create bug for nine months;
a 500 lets clients distinguish between "bad input" and "server fault".
- The `IntegrityError` branch no longer falls through silently when the
message is unrecognised. It re-raises so the catch-all `except` logs
the exception and returns a 500.
- `transaction.on_commit()` now schedules `model_activity.delay` via
`functools.partial` instead of a lambda, avoiding late-binding closure
semantics.
- `ProjectCreateSerializer.validate()` now rejects `project_lead`
values that are not active workspace members, surfacing the error
under the `project_lead` field key (rather than as `non_field_errors`)
so API clients can react programmatically.
* test(api): harden assertions and cover rollback / workspace-membership
Address review feedback from @sriramveeraghanta on PR #8966:
- The three existing tests now look up the created project via
`Project.objects.get(id=response.data["id"])` instead of
`.first()`. The assertion now fails for the right reason if the
wrong project is returned by the endpoint.
- New `test_create_project_with_lead_not_in_workspace_returns_400`
guards the workspace-membership validation added to
`ProjectCreateSerializer.validate()`. Expects a 400 with a
field-shaped error and zero rows persisted.
- New `test_model_activity_not_called_on_rollback` locks in the
`transaction.on_commit()` semantics: when an exception is raised
inside the atomic block (forced via mocking `State.objects.bulk_create`),
the response is 500, no Project / ProjectMember / State rows are
persisted, and the deferred `model_activity.delay` task is never
dispatched. This prevents a future refactor from silently
regressing the rollback contract.
* fix(api): mark on_commit dispatch as robust against broker failures
Address coderabbit re-review feedback on PR #8966.
Without robust=True, an exception raised by model_activity.delay
(e.g., a Celery broker outage) propagates out of the on_commit
callback and is caught by the outer `except Exception` handler,
which returns a 500 despite the project, ProjectMember rows and
default States having already been committed. The client sees a
500 and assumes the create failed — the same class of mismatch
between actual state and reported status that the original bug
exhibited, just at the post-commit phase.
Set robust=True so Django logs the dispatch failure internally
via the standard transaction logger and the response stays 201,
reflecting the persisted state.
Switch from `functools.partial` to a nested function
(`_dispatch_model_activity`) for the on_commit callable. Django's
robust on_commit logging path reads `func.__qualname__` to format
the error message; `partial` objects lack that dunder by default,
and the `functools.update_wrapper` workaround turns out to be
brittle when the wrapped callable is replaced by a Mock (which
the new regression test relies on). A nested function exposes
`__qualname__` natively, and the locals it closes over are
bound at definition time and never rebound before the callback
fires, so the late-binding-closure motivation for `partial` over
`lambda` does not apply here.
A new test, test_response_still_201_when_broker_dispatch_fails,
mirrors test_model_activity_not_called_on_rollback to lock in the
post-commit branch. It uses `@pytest.mark.django_db(transaction=True)`
so the surrounding test transaction is actually committed and the
`on_commit` callback fires (the default wrapper suppresses it via
rollback).
* fix(api): handle unrecognised IntegrityError consistently
Address coderabbit re-review feedback on PR #8966.
The previous fix used `raise` inside the IntegrityError handler with
the intent of "letting the catch-all `except Exception` below log it
and return 500". Coderabbit correctly flagged that `raise` exits the
try/except entirely — sibling except clauses don't fire — so
unrecognised integrity errors actually skipped `log_exception` and
the consistent 500 JSON shape, contradicting the stated intent.
Replicate the catch-all behaviour inline: log the exception via
`log_exception(e)` and return the same generic 500 response with
`{"error": "An unexpected error occurred"}`. The client now gets a
uniform error shape regardless of which `except` branch handled it.
---------
Co-authored-by: Jose Antonio Martinez <257598434+jamartineztelecoengineer84-dotcom@users.noreply.github.com>