Skip to content
Merged

to main #1016

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

## Tech Stack
- **Runtime:** Node.js
- **Framework:** Express (likely, based on structure)
- **Testing:** Vitest (vitest.config.ts found)
- **Linting:** ESLint (eslint.config.mjs found)
- **Framework:** Express
- **Testing:** Vitest (vitest.config.ts)
- **Linting:** ESLint (eslint.config.mjs)

## Development Workflow
- **Build:** Check package.json for build scripts.
- **Standards:** Follow existing ESM patterns.
- **Merging:** Gemini is **NOT** allowed to merge PR changes to the `dev` or `main` branches. The user is the reviewer.

## Routes & Verbs
- **Venue Updates (`/venue/:id`)**: `PATCH /venue/:id` is the standard partial-merge update verb. `PUT /venue/:id` is maintained alongside `PATCH` for backward compatibility, both routing to `controller.updateVenue`. Address updates enforce immutability once set (`400: address cannot be removed`).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web-jam-back",
"version": "2.10.3",
"version": "2.11.0",
"description": "web-jam.com",
"type": "module",
"main": "build/src/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/model/venue/venue-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ class VenueController extends Controller {
return null;
}

// PUT /venue/:id — partial update. See applyAddressUpdate above for the
// PATCH /venue/:id (and legacy PUT /venue/:id) — partial update. See applyAddressUpdate above for the
// #987 address-immutability rule this enforces.
async updateVenue(req: AuthIdRequest, res: Response): Promise<unknown> {
const guardErr = await this.authorize(req, ['venue:edit']);
Expand Down
4 changes: 4 additions & 0 deletions src/model/venue/venue-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ router.route('/:id')
const action = routeUtils.makeAction(req, res, 'updateVenue', controller, authUtils);
void action();
})
.patch((req, res) => {
const action = routeUtils.makeAction(req, res, 'updateVenue', controller, authUtils);
void action();
})
.delete((req, res) => {
const action = routeUtils.makeAction(req, res, 'deleteVenue', controller, authUtils);
void action();
Expand Down
96 changes: 96 additions & 0 deletions test/unit/venue/venue-router.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import app from '#src/index.js';
import venueModel from '#src/model/venue/venue-facade.js';
import userModel from '#src/model/user/user-facade.js';
import authUtils from '#src/auth/authUtils.js';
import request, { type ApiResponse } from '../../helpers/api.js';

describe('Venue Router (PATCH and PUT /venue/:id)', () => {
let r: ApiResponse, agentUser: { _id: string };
const allowedUrl = JSON.parse(process.env.AllowUrl || '{}').urls[0];

beforeAll(async () => {
await venueModel.deleteMany({});
await userModel.deleteMany({});
const createdUser = await userModel.create({
name: 'agent-test',
email: 'agent-venue-router@example.com',
privileges: ['venue:create', 'venue:edit', 'venue:delete'],
}) as unknown as { _id: { toString(): string } };
agentUser = { _id: createdUser._id.toString() };
});

beforeEach(async () => {
await venueModel.deleteMany({});
});

it('updates a venue using PATCH /venue/:id with partial-merge semantics (#990)', async () => {
const venue = await venueModel.create({
name: 'The Spot on Kirk',
address: '22 S Kirk St',
city: 'Roanoke',
usState: 'Virginia',
phone: '540-555-0100',
}) as unknown as { _id: { toString(): string }; name: string; city: string; phone: string };

r = await request(app)
.patch(`/venue/${venue._id.toString()}`)
.set({ origin: allowedUrl })
.set('Authorization', `Bearer ${authUtils.createJWT({ _id: agentUser._id })}`)
.send({ phone: '540-555-0199' });

expect(r.status).toBe(200);
expect(r.body.phone).toBe('540-555-0199');
expect(r.body.name).toBe('The Spot on Kirk');
expect(r.body.city).toBe('Roanoke');
});

it('updates a venue using PUT /venue/:id with identical partial-merge semantics (#990)', async () => {
const venue = await venueModel.create({
name: 'The Spot on Kirk',
address: '22 S Kirk St',
city: 'Roanoke',
usState: 'Virginia',
phone: '540-555-0100',
}) as unknown as { _id: { toString(): string }; name: string; city: string; phone: string };

r = await request(app)
.put(`/venue/${venue._id.toString()}`)
.set({ origin: allowedUrl })
.set('Authorization', `Bearer ${authUtils.createJWT({ _id: agentUser._id })}`)
.send({ phone: '540-555-0200' });

expect(r.status).toBe(200);
expect(r.body.phone).toBe('540-555-0200');
expect(r.body.name).toBe('The Spot on Kirk');
expect(r.body.city).toBe('Roanoke');
});

it('enforces address validation rules identically on both PATCH and PUT /venue/:id (#987/#990)', async () => {
const venue = await venueModel.create({
name: 'The Spot on Kirk',
address: '22 S Kirk St',
city: 'Roanoke',
usState: 'Virginia',
}) as unknown as { _id: { toString(): string } };

// Attempting to remove address when one is already set fails on PATCH
r = await request(app)
.patch(`/venue/${venue._id.toString()}`)
.set({ origin: allowedUrl })
.set('Authorization', `Bearer ${authUtils.createJWT({ _id: agentUser._id })}`)
.send({ address: '' });

expect(r.status).toBe(400);
expect(r.body.message).toContain('cannot be removed');

// Attempting to remove address when one is already set fails identically on PUT
r = await request(app)
.put(`/venue/${venue._id.toString()}`)
.set({ origin: allowedUrl })
.set('Authorization', `Bearer ${authUtils.createJWT({ _id: agentUser._id })}`)
.send({ address: '' });

expect(r.status).toBe(400);
expect(r.body.message).toContain('cannot be removed');
});
});
Loading