diff --git a/api/changelogs/changelog_v2_3_0 .md b/api/changelogs/changelog_v2_3_0 .md new file mode 100644 index 00000000..8f68f8ba --- /dev/null +++ b/api/changelogs/changelog_v2_3_0 .md @@ -0,0 +1,25 @@ +## Info +### Version +v2.3.0 +### Date +2025-19-07 +### Autor +James Stark +## Changelog: + +### Endpoints +- Add new POST endpoint to matching to enable demo functionality +- Add new GET endpoint to user to enable fetching pre-made demo users + + +``` diff + +@@ POST @ /api/v2/matching/demo @@ ++ Add new POST operation with a MatchRequestNew payload +# Generates a match fitting this MatchRequest with placeholder users + +@@ Get @ /api/v2/users/demo @@ ++ Add new GET operation with no payload that returns demo users +# Returns 3 pre-made demo-users + +``` \ No newline at end of file diff --git a/api/changelogs/changelog_v2_3_1.md b/api/changelogs/changelog_v2_3_1.md new file mode 100644 index 00000000..601d5b1a --- /dev/null +++ b/api/changelogs/changelog_v2_3_1.md @@ -0,0 +1,20 @@ +## Info +### Version +v2.3.0 +### Date +2025-19-07 +### Autor +James Stark +## Changelog: + +### Endpoints +- Remove internal tag + + +``` diff + +@@ GET @ /api/v2/users/demo @@ +- Remove internal tag +# code-gen does not generate internal + +``` \ No newline at end of file diff --git a/api/flags/flags.yaml b/api/flags/flags.yaml index f9ec9109..ffdc847c 100644 --- a/api/flags/flags.yaml +++ b/api/flags/flags.yaml @@ -3,4 +3,4 @@ # DO NOT CHANGE info: - version: 2.2.2 + version: 2.3.1 diff --git a/api/openapi.yaml b/api/openapi.yaml index 65b5fd15..4b8587c9 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -3,7 +3,7 @@ x-stoplight: id: ceylawji1yc2t info: title: MeetAtMensa - version: 2.2.2 + version: 2.3.1 description: |- This OpenAPI specification defines the endpoints, schemas, and security mechanisms for the Meet@Mensa User micro-service. @@ -1063,6 +1063,50 @@ paths: x-stoplight: id: 3fke7st84x7x9 description: Retrieve a user object based on an Auth0 sub ID + /api/v2/matching/demo: + post: + summary: Create demo request + tags: + - Matching + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '409': + description: User already has a meeting on this day! + '500': + description: Internal Server Error + operationId: post-api-v2-matching-demo + x-stoplight: + id: nz0sz8ks59ugw + description: Submit a match request which will be immediately matched with a group of demo users. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatchRequestNew' + /api/v2/users/demo: + get: + summary: Get demo users + tags: + - User + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserCollection' + '500': + description: Internal Server Error + operationId: get-api-v2-users-demo + x-stoplight: + id: fq2onjttsvxxa + description: Return 3 demo-users in a UserCollection + x-internal: false tags: - name: GenAI description: Paths belonging to the GenAI microservice diff --git a/client/src/__tests__/components/CreateMatchRequestDialog.test.tsx b/client/src/__tests__/components/CreateMatchRequestDialog.test.tsx index 0e6def3c..3358fb21 100644 --- a/client/src/__tests__/components/CreateMatchRequestDialog.test.tsx +++ b/client/src/__tests__/components/CreateMatchRequestDialog.test.tsx @@ -125,4 +125,126 @@ describe('CreateMatchRequestDialog', () => { expect(mockOnClose).toHaveBeenCalled(); }); + + it('submits demo form with valid data when demo button is clicked', async () => { + const user = userEvent.setup(); + const mockOnDemoSubmit = jest.fn(); + + render( + + ); + + // Fill in location + const locationSelect = screen.getByRole('combobox'); + await user.click(locationSelect); + await user.click(screen.getByText('Mensa Garching')); + + // Set date + const datePickerButton = screen.getByRole('button', { name: /choose date/i }); + await user.click(datePickerButton); + await user.keyboard('{Enter}'); + + // Select timeslots + const timeslotButtons = screen + .getAllByRole('button') + .filter( + (button) => + button.textContent?.includes('12:00-12:15') || + button.textContent?.includes('12:15-12:30') || + button.textContent?.includes('12:30-12:45') + ); + + for (const button of timeslotButtons.slice(0, 3)) { + await user.click(button); + } + + // Wait for demo button to be enabled + await waitFor( + () => { + const demoButton = screen.getByRole('button', { name: /demo/i }); + expect(demoButton).not.toBeDisabled(); + }, + { timeout: 5000 } + ); + + // Click demo button + const demoButton = screen.getByRole('button', { name: /demo/i }); + await user.click(demoButton); + + await waitFor(() => { + expect(mockOnDemoSubmit).toHaveBeenCalledWith({ + userID: 'test-user-id', + location: 'GARCHING', + date: expect.stringMatching(/^[\d]{4}-[\d]{2}-[\d]{2}$/), + timeslot: [9, 10, 11], + preferences: { + degreePref: false, + agePref: false, + genderPref: false, + }, + }); + }); + }); + + it('falls back to regular submit when demo handler is not provided', async () => { + const user = userEvent.setup(); + + render(); + + // Fill in location + const locationSelect = screen.getByRole('combobox'); + await user.click(locationSelect); + await user.click(screen.getByText('Mensa Garching')); + + // Set date + const datePickerButton = screen.getByRole('button', { name: /choose date/i }); + await user.click(datePickerButton); + await user.keyboard('{Enter}'); + + // Select timeslots + const timeslotButtons = screen + .getAllByRole('button') + .filter( + (button) => + button.textContent?.includes('12:00-12:15') || + button.textContent?.includes('12:15-12:30') || + button.textContent?.includes('12:30-12:45') + ); + + for (const button of timeslotButtons.slice(0, 3)) { + await user.click(button); + } + + // Wait for demo button to be enabled + await waitFor( + () => { + const demoButton = screen.getByRole('button', { name: /demo/i }); + expect(demoButton).not.toBeDisabled(); + }, + { timeout: 5000 } + ); + + // Click demo button (should fall back to regular submit) + const demoButton = screen.getByRole('button', { name: /demo/i }); + await user.click(demoButton); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalledWith({ + userID: 'test-user-id', + location: 'GARCHING', + date: expect.stringMatching(/^[\d]{4}-[\d]{2}-[\d]{2}$/), + timeslot: [9, 10, 11], + preferences: { + degreePref: false, + agePref: false, + genderPref: false, + }, + }); + }); + }); }); diff --git a/client/src/api.ts b/client/src/api.ts index c3cbe417..ef2df0cd 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -237,6 +237,46 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v2/matching/demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create demo request + * @description Submit a match request which will be immediately matched with a group of demo users. + */ + post: operations["post-api-v2-matching-demo"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v2/users/demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get demo users + * @description Return 3 demo-users in a UserCollection + */ + get: operations["get-api-v2-users-demo"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -1110,4 +1150,69 @@ export interface operations { }; }; }; + "post-api-v2-matching-demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["MatchRequestNew"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Group"]; + }; + }; + /** @description User already has a meeting on this day! */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + "get-api-v2-users-demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserCollection"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; } diff --git a/client/src/components/CreateMatchRequestDialog.tsx b/client/src/components/CreateMatchRequestDialog.tsx index 8b3f46a5..75b04ca5 100644 --- a/client/src/components/CreateMatchRequestDialog.tsx +++ b/client/src/components/CreateMatchRequestDialog.tsx @@ -55,12 +55,14 @@ interface CreateMatchRequestDialogProps { open: boolean; onClose: () => void; onSubmit: (matchRequestData: any) => void; // TODO: Define proper type + onDemoSubmit?: (matchRequestData: any) => void; // Optional demo submission handler } const CreateMatchRequestDialog: React.FC = ({ open, onClose, onSubmit, + onDemoSubmit, }) => { const userID = useUserID(); const [selectedLocation, setSelectedLocation] = useState(''); @@ -145,6 +147,29 @@ const CreateMatchRequestDialog: React.FC = ({ }); }; + const handleDemoSubmit = () => { + if (!selectedLocation || !selectedDate || !areTimeslotsConsecutive(selectedTimeslots) || !userID) { + return; + } + + const formattedDate = selectedDate.toISOString().split('T')[0]; // YYYY-MM-DD format + + const demoData = { + userID: userID, + date: formattedDate, + timeslot: selectedTimeslots, + location: selectedLocation.toUpperCase(), + preferences, + }; + + if (onDemoSubmit) { + onDemoSubmit(demoData); + } else { + // Fallback to regular submit if no demo handler provided + onSubmit(demoData); + } + }; + const disabledTimeslots = getDisabledTimeslots(selectedDate); return ( @@ -237,6 +262,16 @@ const CreateMatchRequestDialog: React.FC = ({ +

Request samples

Content type
application/json
{
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    }
}

Response samples

Content type
application/json
{
  • "requestID": "e4619679-f5d9-4eff-9f79-bbded6130bb1",
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    },
  • "status": "PENDING"
}

Retrieve all matches for a {user-id}

Request samples

Content type
application/json
{
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    }
}

Response samples

Content type
application/json
{
  • "requestID": "e4619679-f5d9-4eff-9f79-bbded6130bb1",
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    },
  • "status": "PENDING"
}

Retrieve all matches for a {user-id}

Retrieve all matches for a user with {user-id} from the matching-service

Authorizations:
jwt-bearer
path Parameters
user-id
required
string <uuid>

UUID associated with a given user

@@ -512,7 +512,7 @@ " class="sc-eVqvcJ sc-fszimp sc-etsjJW kIppRw jnwENr ljKHqG">

The requested resource was not found.

Response samples

Content type
application/json
{
  • "matches": [
    ]
}

Retrieve all MatchRequests for a {user-id}

Response samples

Content type
application/json
{
  • "matches": [
    ]
}

Retrieve all MatchRequests for a {user-id}

Retrieve all MatchRequests for a user with {user-id} from the matching-service

Authorizations:
jwt-bearer
path Parameters
user-id
required
string <uuid>

UUID associated with a given user

@@ -526,7 +526,7 @@ " class="sc-eVqvcJ sc-fszimp sc-etsjJW kIppRw jnwENr ljKHqG">

The requested resource was not found.

Response samples

Content type
application/json
{
  • "requests": [
    ]
}

Delete MatchRequest with {request-id}

Response samples

Content type
application/json
{
  • "requests": [
    ]
}

Delete MatchRequest with {request-id}

Delete MatchRequest with ID {request-id} from the system

Authorizations:
jwt-bearer
path Parameters
request-id
required
string <uuid>

UUID associated with a given match request

@@ -594,7 +594,7 @@ " class="sc-eVqvcJ sc-fszimp sc-etsjJW kIppRw jnwENr ljKHqG">

MatchRequest cannot be updated since it has already been fulfilled!

Request samples

Content type
application/json
{
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    }
}

Response samples

Content type
application/json
{
  • "requestID": "e4619679-f5d9-4eff-9f79-bbded6130bb1",
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    },
  • "status": "PENDING"
}

Accept invitation to a given match

Request samples

Content type
application/json
{
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    }
}

Response samples

Content type
application/json
{
  • "requestID": "e4619679-f5d9-4eff-9f79-bbded6130bb1",
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    },
  • "status": "PENDING"
}

Accept invitation to a given match

Accept invitation to a given match

Authorizations:
jwt-bearer
path Parameters
match-id
required
string <uuid>

UUID associated with a given match

@@ -618,7 +618,55 @@ " class="sc-eVqvcJ sc-fszimp sc-etsjJW kIppRw jnwENr ljKHqG">

The requested resource was not found.

User

Create demo request

Submit a match request which will be immediately matched with a group of demo users.

+
Authorizations:
jwt-bearer
Request Body schema: application/json
userID
required
string <uuid> (userID)

The unique ID of a single student in the Meet@Mensa system.

+
date
required
string <date>

The date a user would like meet@mensa to find them a match

+
timeslot
required
Array of integers (timeslot) [ items [ 1 .. 16 ] ]
location
required
any (location)
Enum: "GARCHING" "ARCISSTR"

Enumerator representing a mensa at which a meet can happen

+ + + + + + + + + + + + + + + +
ValueDescription
GARCHINGThe Mensa at the TUM Garching Campus
ARCISSTRThe Mensa at the TUM Innenstadt Campus (Arcisstr. 21)
+
required
object (MatchPreferences)

Object Representing a set of user preferences

+

Responses

Request samples

Content type
application/json
{
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "date": "2019-08-24",
  • "timeslot": [
    ],
  • "location": "GARCHING",
  • "preferences": {
    }
}

Response samples

Content type
application/json
{
  • "groupID": "ec414289-a6cd-4a76-a6d7-c7f42c7f1517",
  • "date": "2019-08-24",
  • "time": 1,
  • "location": "GARCHING",
  • "userStatus": [
    ],
  • "conversationStarters": {
    }
}

User

Paths belonging to the User microservice

Retrieve User with {user-id}

Fetch all information about user with ID {user-id} from user-service

@@ -724,9 +772,15 @@ " class="sc-eVqvcJ sc-fszimp sc-etsjJW kIppRw jnwENr ljKHqG">

Forbidden

Response samples

Content type
application/json
{
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "email": "user@example.com",
  • "firstname": "Max",
  • "lastname": "Mustermann",
  • "birthday": "2019-08-24",
  • "gender": "other",
  • "degree": "msc_informatics",
  • "degreeStart": 2024,
  • "interests": [
    ],
  • "bio": "string"
}
+

Response samples

Content type
application/json
{
  • "userID": "2c3821b8-1cdb-4b77-bcd8-a1da701e46aa",
  • "email": "user@example.com",
  • "firstname": "Max",
  • "lastname": "Mustermann",
  • "birthday": "2019-08-24",
  • "gender": "other",
  • "degree": "msc_informatics",
  • "degreeStart": 2024,
  • "interests": [
    ],
  • "bio": "string"
}

Get demo users

Return 3 demo-users in a UserCollection

+
Authorizations:
jwt-bearer

Responses

Response samples

Content type
application/json
{
  • "users": [
    ]
}