test: guard inactive directory membership
This commit is contained in:
@@ -117,3 +117,45 @@ Passed with no whitespace errors.
|
|||||||
- Shared-team membership now uses every requester `Player` row, including inactive ones, exactly as required by the directory plan.
|
- Shared-team membership now uses every requester `Player` row, including inactive ones, exactly as required by the directory plan.
|
||||||
- The user query joins only `status` and `role`, projects safe raw columns, applies shared-team visibility/search in SQL, counts `DISTINCT user.id`, orders by `user.id`, and applies offset/limit before mapping.
|
- The user query joins only `status` and `role`, projects safe raw columns, applies shared-team visibility/search in SQL, counts `DISTINCT user.id`, orders by `user.id`, and applies offset/limit before mapping.
|
||||||
- Assignment rows are fetched only for the selected page of user IDs and are scoped with the same shared-team subquery for non-admins. No directory query selects or hydrates `User` authentication columns.
|
- Assignment rows are fetched only for the selected page of user IDs and are scoped with the same shared-team subquery for non-admins. No directory query selects or hydrates `User` authentication columns.
|
||||||
|
|
||||||
|
## Fix Round 2
|
||||||
|
|
||||||
|
### Files changed
|
||||||
|
|
||||||
|
- `myteamwallet_backend/src/users/users.service.spec.ts` — strengthens the inactive-requester regression with a QueryBuilder boundary that rejects `requesterPlayer.active` in the shared-team predicate.
|
||||||
|
|
||||||
|
### RED evidence
|
||||||
|
|
||||||
|
Test file: `myteamwallet_backend/src/users/users.service.spec.ts`
|
||||||
|
|
||||||
|
After installing the boundary guard, the shared-team query was deliberately mutated to add `requesterPlayer.active = :active`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test -- users/users.service.spec.ts --runInBand
|
||||||
|
```
|
||||||
|
|
||||||
|
Result: failed as expected, 1/9 tests failed. `treats an inactive requester assignment as a shared team membership` failed with `shared-team membership must not filter inactive requester assignments`. The mutation was then removed; the production query remains user-ID-only.
|
||||||
|
|
||||||
|
### GREEN verification
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm test -- users/users.service.spec.ts --runInBand
|
||||||
|
```
|
||||||
|
|
||||||
|
Passed: 1 suite, 9 tests.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\node_modules\.bin\eslint.cmd src\users\users.service.spec.ts --max-warnings=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Passed with no warnings or errors.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Passed with no whitespace errors.
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- The test double checks the actual shared-team predicate supplied by the service, rather than returning fixed rows alone. It rejects only for the inactive-requester regression if a predicate references `requesterPlayer.active`, so the test now fails for the realistic authorization regression while preserving the existing output-contract assertions.
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('UsersService directory', () => {
|
|||||||
let userRows: ReturnType<typeof directoryUserRow>[];
|
let userRows: ReturnType<typeof directoryUserRow>[];
|
||||||
let assignmentRows: ReturnType<typeof directoryAssignmentRow>[];
|
let assignmentRows: ReturnType<typeof directoryAssignmentRow>[];
|
||||||
let total: number;
|
let total: number;
|
||||||
|
let rejectActiveSharedTeamPredicate: boolean;
|
||||||
const usersRepository = {
|
const usersRepository = {
|
||||||
find: jest.fn(() => {
|
find: jest.fn(() => {
|
||||||
throw new Error('directory queries must use a safe database projection');
|
throw new Error('directory queries must use a safe database projection');
|
||||||
@@ -55,6 +56,7 @@ describe('UsersService directory', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
rejectActiveSharedTeamPredicate = false;
|
||||||
setDirectoryResult(
|
setDirectoryResult(
|
||||||
[users[0], users[1], users[2], users[4]],
|
[users[0], users[1], users[2], users[4]],
|
||||||
[players[0], players[1], players[2], players[4]],
|
[players[0], players[1], players[2], players[4]],
|
||||||
@@ -105,6 +107,7 @@ describe('UsersService directory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('treats an inactive requester assignment as a shared team membership', async () => {
|
it('treats an inactive requester assignment as a shared team membership', async () => {
|
||||||
|
rejectActiveSharedTeamPredicate = true;
|
||||||
setDirectoryResult(
|
setDirectoryResult(
|
||||||
[users[0], users[1]],
|
[users[0], users[1]],
|
||||||
[
|
[
|
||||||
@@ -353,7 +356,17 @@ describe('UsersService directory', () => {
|
|||||||
function createSharedTeamsQuery() {
|
function createSharedTeamsQuery() {
|
||||||
const query: any = {
|
const query: any = {
|
||||||
select: () => query,
|
select: () => query,
|
||||||
where: () => query,
|
where: (predicate: string) => {
|
||||||
|
if (
|
||||||
|
rejectActiveSharedTeamPredicate &&
|
||||||
|
/requesterPlayer\.active/i.test(predicate)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'shared-team membership must not filter inactive requester assignments',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
},
|
||||||
getQuery: () =>
|
getQuery: () =>
|
||||||
'SELECT requesterPlayer.teamId FROM player requesterPlayer',
|
'SELECT requesterPlayer.teamId FROM player requesterPlayer',
|
||||||
getParameters: () => ({ requesterId: users[0].id }),
|
getParameters: () => ({ requesterId: users[0].id }),
|
||||||
|
|||||||
Reference in New Issue
Block a user