diff --git a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java index a15523d3b..b6f9093f3 100644 --- a/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java +++ b/src/main/java/org/opentripplanner/middleware/OtpMiddlewareMain.java @@ -18,6 +18,7 @@ import org.opentripplanner.middleware.controllers.api.OtpUserController; import org.opentripplanner.middleware.controllers.api.TrackedTripController; import org.opentripplanner.middleware.controllers.api.TripHistoryController; +import org.opentripplanner.middleware.controllers.api.TripSurveyController; import org.opentripplanner.middleware.docs.PublicApiDocGenerator; import org.opentripplanner.middleware.models.MonitoredComponent; import org.opentripplanner.middleware.otp.OtpVersion; @@ -120,18 +121,18 @@ private static void initializeHttpEndpoints() throws IOException { .endpoints(() -> List.of( new AdminUserController(API_PREFIX), new ApiUserController(API_PREFIX), + new CDPFilesController(API_PREFIX), new CDPUserController(API_PREFIX), + new ErrorEventsController(API_PREFIX), + new LogController(API_PREFIX), + new MonitoredComponentController(API_PREFIX), new MonitoredTripController(API_PREFIX), + new OtpRequestProcessor("/otp", OtpVersion.OTP2), + new OtpRequestProcessor("/otp2", OtpVersion.OTP2), + new OtpUserController(API_PREFIX), new TrackedTripController(API_PREFIX), new TripHistoryController(API_PREFIX), - new MonitoredComponentController(API_PREFIX), - new OtpUserController(API_PREFIX), - new LogController(API_PREFIX), - new ErrorEventsController(API_PREFIX), - new CDPFilesController(API_PREFIX), - new OtpRequestProcessor("/otp", OtpVersion.OTP2), - new OtpRequestProcessor("/otp2", OtpVersion.OTP2) - // Add other endpoints as needed. + new TripSurveyController(API_PREFIX) )) // Spark-swagger auto-generates a swagger document at localhost:4567/doc.yaml. // (That path is not configurable.) diff --git a/src/main/java/org/opentripplanner/middleware/controllers/api/TripSurveyController.java b/src/main/java/org/opentripplanner/middleware/controllers/api/TripSurveyController.java new file mode 100644 index 000000000..35a897050 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/controllers/api/TripSurveyController.java @@ -0,0 +1,125 @@ +package org.opentripplanner.middleware.controllers.api; + +import io.github.manusant.ss.SparkSwagger; +import io.github.manusant.ss.rest.Endpoint; +import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.models.MonitoredTrip; +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.models.TripSurveyNotification; +import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.JsonUtils; +import spark.Request; +import spark.Response; + +import java.time.Instant; +import java.util.Date; +import java.util.Optional; + +import static io.github.manusant.ss.descriptor.EndpointDescriptor.endpointPath; +import static io.github.manusant.ss.descriptor.MethodDescriptor.path; +import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; +import static org.opentripplanner.middleware.utils.NotificationUtils.TRIP_SURVEY_ID; +import static org.opentripplanner.middleware.utils.NotificationUtils.TRIP_SURVEY_SUBDOMAIN; + +public class TripSurveyController implements Endpoint { + private final String ROOT_ROUTE; + + private static final String OPEN_PATH = "/open"; + + public TripSurveyController(String apiPrefix) { + this.ROOT_ROUTE = apiPrefix + "trip-survey"; + } + + /** + * Register the API endpoint and GET resource that redirects to a survey form. + */ + @Override + public void bind(final SparkSwagger restApi) { + restApi.endpoint( + endpointPath(ROOT_ROUTE).withDescription("Interface for tracking opened trip surveys following a trip survey notification."), + HttpUtils.NO_FILTER + ) + .get( + path(ROOT_ROUTE + OPEN_PATH) + .withDescription("Generates a tracking survey link for a specified user, trip, notification ids.") + .withQueryParam().withName("user_id").withRequired(true).withDescription("The id of the OtpUser that this notification applies to.").and() + .withQueryParam().withName("trip_id").withRequired(true).withDescription("The id of the MonitoredTrip that this notification applies to.").and() + .withQueryParam().withName("notification_id").withRequired(true).withDescription("The id of the notification that this notification applies to.").and(), + TripSurveyController::processCall, JsonUtils::toJson + ); + } + + /** + * Check that the requested survey is valid (user, trip, and notifications point to existing data). + */ + private static OtpUser checkParameters(String userId, String tripId, String notificationId, Request request) { + OtpUser user = Persistence.otpUsers.getById(userId); + if (user == null) { + returnInvalidUrlParametersError(request); + } else { + Optional notificationOpt = user.findNotification(notificationId); + if (notificationOpt.isEmpty()) returnInvalidUrlParametersError(request); + + MonitoredTrip trip = Persistence.monitoredTrips.getById(tripId); + if (trip == null) returnInvalidUrlParametersError(request); + } + + return user; + } + + private static void returnInvalidUrlParametersError(Request request) { + logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Invalid URL parameters"); + } + + /** + * Mark notification as opened, but if an opened date is already populated, do nothing. + */ + private static void updateNotificationStateIfNeeded(OtpUser user, String notificationId) { + TripSurveyNotification notification = user.findNotification(notificationId).orElse(null); + if (notification != null && notification.timeOpened == null) { + notification.timeOpened = Date.from(Instant.now()); + Persistence.otpUsers.replace(user.id, user); + } + } + + public static String makeTripSurveyUrl(String subdomain, String surveyId, String userId, String tripId, String notificationId) { + // Parameters have been checked before, so there shouldn't be a need to encode parameters. + return String.format( + "https://%s.typeform.com/to/%s#user_id=%s&trip_id=%s¬ification_id=%s", + subdomain, + surveyId, + userId, + tripId, + notificationId + ); + } + + private static boolean processCall(Request req, Response res) { + String userId = req.queryParams("user_id"); + String tripId = req.queryParams("trip_id"); + String notificationId = req.queryParams("notification_id"); + + OtpUser user = checkParameters(userId, tripId, notificationId, req); + + if (user != null) { + String surveyUrl = makeTripSurveyUrl( + TRIP_SURVEY_SUBDOMAIN, + TRIP_SURVEY_ID, + userId, + tripId, + notificationId + ); + + // Update notification state + updateNotificationStateIfNeeded(user, notificationId); + + // Redirect + res.redirect(surveyUrl); + + return true; + } + + return false; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java index 1c9e32a02..bd2ae4b16 100644 --- a/src/main/java/org/opentripplanner/middleware/models/OtpUser.java +++ b/src/main/java/org/opentripplanner/middleware/models/OtpUser.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; +import org.apache.logging.log4j.util.Strings; import org.opentripplanner.middleware.auth.Auth0Users; import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.persistence.Persistence; @@ -210,4 +211,10 @@ public Optional findLastTripSurveyNotificationSent() { if (tripSurveyNotifications == null) return Optional.empty(); return tripSurveyNotifications.stream().max(Comparator.comparingLong(n -> n.timeSent.getTime())); } + + /** Obtains a notification with the given id, if available. */ + public Optional findNotification(String id) { + if (tripSurveyNotifications == null || Strings.isBlank(id)) return Optional.empty(); + return tripSurveyNotifications.stream().filter(n -> id.equals(n.id)).findFirst(); + } } diff --git a/src/main/java/org/opentripplanner/middleware/models/TripSurveyNotification.java b/src/main/java/org/opentripplanner/middleware/models/TripSurveyNotification.java index 0d6ec8af0..bb6d7f621 100644 --- a/src/main/java/org/opentripplanner/middleware/models/TripSurveyNotification.java +++ b/src/main/java/org/opentripplanner/middleware/models/TripSurveyNotification.java @@ -1,7 +1,6 @@ package org.opentripplanner.middleware.models; import java.util.Date; -import java.util.UUID; /** Contains information regarding survey notifications sent after a trip is completed. */ public class TripSurveyNotification { @@ -17,6 +16,9 @@ public class TripSurveyNotification { /** Date/time when the trip survey notification was sent. */ public Date timeSent; + /** Date/time when the trip survey notification was opened. */ + public Date timeOpened; + /** The {@link TrackedJourney} (and, indirectly, the {@link MonitoredTrip}) that this notification refers to. */ public String journeyId; diff --git a/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java b/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java index 251f39240..265790f21 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java @@ -64,8 +64,8 @@ public class NotificationUtils { public static final String OTP_ADMIN_DASHBOARD_FROM_EMAIL = getConfigPropertyAsText("OTP_ADMIN_DASHBOARD_FROM_EMAIL"); private static final String PUSH_API_KEY = getConfigPropertyAsText("PUSH_API_KEY"); private static final String PUSH_API_URL = getConfigPropertyAsText("PUSH_API_URL"); - private static final String TRIP_SURVEY_ID = getConfigPropertyAsText("TRIP_SURVEY_ID"); - private static final String TRIP_SURVEY_SUBDOMAIN = getConfigPropertyAsText("TRIP_SURVEY_SUBDOMAIN"); + public static final String TRIP_SURVEY_ID = getConfigPropertyAsText("TRIP_SURVEY_ID"); + public static final String TRIP_SURVEY_SUBDOMAIN = getConfigPropertyAsText("TRIP_SURVEY_SUBDOMAIN"); private static final String OTP_UI_NAME = ConfigUtils.getConfigPropertyAsText("OTP_UI_NAME"); private static final String OTP_UI_URL = ConfigUtils.getConfigPropertyAsText("OTP_UI_URL"); private static final String TRIPS_PATH = ACCOUNT_PATH + "/trips"; diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 60ecf59fd..3e8f05d54 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -18,28 +18,31 @@ tags: description: "Interface for querying and managing 'AdminUser' entities." - name: "api/secure/application" description: "Interface for querying and managing 'ApiUser' entities." +- name: "api/secure/connected-data" + description: "Interface for listing and downloading CDP files from S3." - name: "api/secure/cdp" description: "Interface for querying and managing 'CDPUser' entities." -- name: "api/secure/monitoredtrip" - description: "Interface for querying and managing 'MonitoredTrip' entities." -- name: "api/secure/triprequests" - description: "Interface for retrieving trip requests." -- name: "api/secure/monitoredcomponent" - description: "Interface for querying and managing 'MonitoredComponent' entities." -- name: "api/secure/user" - description: "Interface for querying and managing 'OtpUser' entities." -- name: "api/secure/logs" - description: "Interface for retrieving API logs from AWS." - name: "api/admin/bugsnag/eventsummary" description: "Interface for reporting and retrieving application errors using Bugsnag." -- name: "api/secure/connected-data" - description: "Interface for listing and downloading CDP files from S3." +- name: "api/secure/logs" + description: "Interface for retrieving API logs from AWS." +- name: "api/secure/monitoredcomponent" + description: "Interface for querying and managing 'MonitoredComponent' entities." +- name: "api/secure/monitoredtrip" + description: "Interface for querying and managing 'MonitoredTrip' entities." - name: "otp" description: "Proxy interface for OTP 2 endpoints. Refer to OTP's\ \ API documentation for OTP's supported API resources." - name: "otp2" description: "Proxy interface for OTP 2 endpoints. Refer to OTP's\ \ API documentation for OTP's supported API resources." +- name: "api/secure/user" + description: "Interface for querying and managing 'OtpUser' entities." +- name: "api/secure/triprequests" + description: "Interface for retrieving trip requests." +- name: "api/trip-survey" + description: "Interface for tracking opened trip surveys following a trip survey\ + \ notification." schemes: - "https" paths: @@ -741,6 +744,62 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} + /api/secure/connected-data: + get: + tags: + - "api/secure/connected-data" + description: "Gets a paginated list of CDP zip files in the configured S3 bucket." + produces: + - "application/json" + parameters: + - name: "limit" + in: "query" + description: "If specified, the maximum number of items to return." + required: false + type: "string" + default: "10" + - name: "offset" + in: "query" + description: "If specified, the number of records to skip/offset." + required: false + type: "string" + default: "0" + responses: + "200": + description: "successful operation" + responseSchema: + type: "array" + items: + $ref: "#/definitions/CDPFile" + schema: + type: "array" + items: + $ref: "#/definitions/CDPFile" + /api/secure/connected-data/download: + get: + tags: + - "api/secure/connected-data" + description: "Generates a download link for a specified object within the CDP\ + \ bucket." + produces: + - "application/json" + parameters: + - name: "/download" + in: "query" + description: "The key of the object to generate a link for." + required: true + type: "string" + responses: + "200": + description: "successful operation" + responseSchema: + type: "array" + items: + $ref: "#/definitions/URL" + schema: + type: "array" + items: + $ref: "#/definitions/URL" /api/secure/cdp/fromtoken: get: tags: @@ -1031,54 +1090,83 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/monitoredtrip/checkitinerary: - post: + /api/admin/bugsnag/eventsummary: + get: tags: - - "api/secure/monitoredtrip" - description: "Returns the itinerary existence check results for a monitored\ - \ trip." + - "api/admin/bugsnag/eventsummary" + description: "Gets a paginated list of the latest Bugsnag event summaries." produces: - "application/json" parameters: - - in: "body" - name: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/MonitoredTrip" + - name: "limit" + in: "query" + description: "If specified, the maximum number of items to return." + required: false + type: "string" + default: "10" + - name: "offset" + in: "query" + description: "If specified, the number of records to skip/offset." + required: false + type: "string" + default: "0" responses: "200": - description: "Successful operation" - examples: {} + description: "successful operation" responseSchema: - $ref: "#/definitions/ItineraryExistence" + type: "array" + items: + $ref: "#/definitions/BugsnagEvent" schema: - $ref: "#/definitions/ItineraryExistence" - "400": - description: "The request was not formed properly (e.g., some required parameters\ - \ may be missing). See the details of the returned response to determine\ - \ the exact issue." - examples: {} - "401": - description: "The server was not able to authenticate the request. This\ - \ can happen if authentication headers are missing or malformed, or the\ - \ authentication server cannot be reached." - examples: {} - "403": - description: "The requesting user is not allowed to perform the request." - examples: {} - "404": - description: "The requested item was not found." - examples: {} - "500": - description: "An error occurred while performing the request. Contact an\ - \ API administrator for more information." - examples: {} - /api/secure/monitoredtrip: + type: "array" + items: + $ref: "#/definitions/BugsnagEvent" + /api/secure/logs: get: tags: - - "api/secure/monitoredtrip" - description: "Gets a paginated list of all 'MonitoredTrip' entities." + - "api/secure/logs" + description: "Gets a list of all API usage logs." + produces: + - "application/json" + parameters: + - name: "keyId" + in: "query" + description: "If specified, restricts the search to the specified AWS API\ + \ key ID." + required: false + type: "string" + - name: "startDate" + in: "query" + description: "If specified, the earliest date (format yyyy-MM-dd) for which\ + \ usage logs are retrieved." + required: false + type: "string" + default: "30 days prior to the current date" + pattern: "yyyy-MM-dd" + - name: "endDate" + in: "query" + description: "If specified, the latest date (format yyyy-MM-dd) for which\ + \ usage logs are retrieved." + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" + responses: + "200": + description: "successful operation" + responseSchema: + type: "array" + items: + $ref: "#/definitions/ApiUsageResult" + schema: + type: "array" + items: + $ref: "#/definitions/ApiUsageResult" + /api/secure/monitoredcomponent: + get: + tags: + - "api/secure/monitoredcomponent" + description: "Gets a paginated list of all 'MonitoredComponent' entities." produces: - "application/json" parameters: @@ -1108,8 +1196,8 @@ paths: $ref: "#/definitions/ResponseList" post: tags: - - "api/secure/monitoredtrip" - description: "Creates a 'MonitoredTrip' entity." + - "api/secure/monitoredcomponent" + description: "Creates a 'MonitoredComponent' entity." consumes: - "application/json" produces: @@ -1120,15 +1208,15 @@ paths: description: "Body object description" required: true schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" responses: "200": description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1149,12 +1237,12 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/monitoredtrip/{id}: + /api/secure/monitoredcomponent/{id}: get: tags: - - "api/secure/monitoredtrip" - description: "Returns the 'MonitoredTrip' entity with the specified id, or 404\ - \ if not found." + - "api/secure/monitoredcomponent" + description: "Returns the 'MonitoredComponent' entity with the specified id,\ + \ or 404 if not found." produces: - "application/json" parameters: @@ -1168,9 +1256,9 @@ paths: description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1193,8 +1281,8 @@ paths: examples: {} put: tags: - - "api/secure/monitoredtrip" - description: "Updates and returns the 'MonitoredTrip' entity with the specified\ + - "api/secure/monitoredcomponent" + description: "Updates and returns the 'MonitoredComponent' entity with the specified\ \ id, or 404 if not found." consumes: - "application/json" @@ -1211,15 +1299,15 @@ paths: description: "Body object description" required: true schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" responses: "200": description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1242,9 +1330,9 @@ paths: examples: {} delete: tags: - - "api/secure/monitoredtrip" - description: "Deletes the 'MonitoredTrip' entity with the specified id if it\ - \ exists." + - "api/secure/monitoredcomponent" + description: "Deletes the 'MonitoredComponent' entity with the specified id\ + \ if it exists." produces: - "application/json" parameters: @@ -1258,9 +1346,9 @@ paths: description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" schema: - $ref: "#/definitions/MonitoredTrip" + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1281,11 +1369,12 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/monitoredtrip/starttracking: + /api/secure/monitoredtrip/checkitinerary: post: tags: - "api/secure/monitoredtrip" - description: "Initiates the tracking of a monitored trip." + description: "Returns the itinerary existence check results for a monitored\ + \ trip." produces: - "application/json" parameters: @@ -1294,151 +1383,40 @@ paths: description: "Body object description" required: true schema: - $ref: "#/definitions/StartTrackingPayload" + $ref: "#/definitions/MonitoredTrip" responses: "200": - description: "successful operation" + description: "Successful operation" + examples: {} responseSchema: - $ref: "#/definitions/TrackingResponse" + $ref: "#/definitions/ItineraryExistence" schema: - $ref: "#/definitions/TrackingResponse" - /api/secure/monitoredtrip/updatetracking: - post: - tags: - - "api/secure/monitoredtrip" - description: "Provides tracking updates on a monitored trip." - produces: - - "application/json" - parameters: - - in: "body" - name: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/UpdatedTrackingPayload" - responses: - "200": - description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" - schema: - $ref: "#/definitions/TrackingResponse" - /api/secure/monitoredtrip/track: - post: - tags: - - "api/secure/monitoredtrip" - description: "Starts or updates tracking on a monitored trip." - produces: - - "application/json" - parameters: - - in: "body" - name: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/TrackPayload" - responses: - "200": - description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" - schema: - $ref: "#/definitions/TrackingResponse" - /api/secure/monitoredtrip/endtracking: - post: - tags: - - "api/secure/monitoredtrip" - description: "Terminates the tracking of a monitored trip by the user." - produces: - - "application/json" - parameters: - - in: "body" - name: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/EndTrackingPayload" - responses: - "200": - description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" - schema: - $ref: "#/definitions/EndTrackingResponse" - /api/secure/monitoredtrip/forciblyendtracking: - post: - tags: - - "api/secure/monitoredtrip" - description: "Forcibly terminates tracking of a monitored trip by trip ID." - produces: - - "application/json" - parameters: - - in: "body" - name: "body" - description: "Body object description" - required: true - schema: - $ref: "#/definitions/ForceEndTrackingPayload" - responses: - "200": - description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" - schema: - $ref: "#/definitions/EndTrackingResponse" - /api/secure/triprequests: - get: - tags: - - "api/secure/triprequests" - description: "Gets a paginated list of the most recent trip requests for a user." - produces: - - "application/json" - parameters: - - name: "userId" - in: "query" - description: "The OTP user for which to retrieve trip requests." - required: true - type: "string" - - name: "limit" - in: "query" - description: "If specified, the maximum number of items to return." - required: false - type: "string" - default: "10" - - name: "offset" - in: "query" - description: "If specified, the number of records to skip/offset." - required: false - type: "string" - default: "0" - - name: "fromDate" - in: "query" - description: "If specified, the earliest date (format yyyy-MM-dd) for which\ - \ trip requests are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" - - name: "toDate" - in: "query" - description: "If specified, the latest date (format yyyy-MM-dd) for which\ - \ trip requests are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" - responses: - "200": - description: "successful operation" - responseSchema: - $ref: "#/definitions/TripRequest" - schema: - $ref: "#/definitions/TripRequest" - /api/secure/monitoredcomponent: + $ref: "#/definitions/ItineraryExistence" + "400": + description: "The request was not formed properly (e.g., some required parameters\ + \ may be missing). See the details of the returned response to determine\ + \ the exact issue." + examples: {} + "401": + description: "The server was not able to authenticate the request. This\ + \ can happen if authentication headers are missing or malformed, or the\ + \ authentication server cannot be reached." + examples: {} + "403": + description: "The requesting user is not allowed to perform the request." + examples: {} + "404": + description: "The requested item was not found." + examples: {} + "500": + description: "An error occurred while performing the request. Contact an\ + \ API administrator for more information." + examples: {} + /api/secure/monitoredtrip: get: tags: - - "api/secure/monitoredcomponent" - description: "Gets a paginated list of all 'MonitoredComponent' entities." + - "api/secure/monitoredtrip" + description: "Gets a paginated list of all 'MonitoredTrip' entities." produces: - "application/json" parameters: @@ -1468,8 +1446,8 @@ paths: $ref: "#/definitions/ResponseList" post: tags: - - "api/secure/monitoredcomponent" - description: "Creates a 'MonitoredComponent' entity." + - "api/secure/monitoredtrip" + description: "Creates a 'MonitoredTrip' entity." consumes: - "application/json" produces: @@ -1480,15 +1458,15 @@ paths: description: "Body object description" required: true schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1509,12 +1487,12 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/monitoredcomponent/{id}: + /api/secure/monitoredtrip/{id}: get: tags: - - "api/secure/monitoredcomponent" - description: "Returns the 'MonitoredComponent' entity with the specified id,\ - \ or 404 if not found." + - "api/secure/monitoredtrip" + description: "Returns the 'MonitoredTrip' entity with the specified id, or 404\ + \ if not found." produces: - "application/json" parameters: @@ -1528,9 +1506,9 @@ paths: description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1553,8 +1531,8 @@ paths: examples: {} put: tags: - - "api/secure/monitoredcomponent" - description: "Updates and returns the 'MonitoredComponent' entity with the specified\ + - "api/secure/monitoredtrip" + description: "Updates and returns the 'MonitoredTrip' entity with the specified\ \ id, or 404 if not found." consumes: - "application/json" @@ -1571,15 +1549,15 @@ paths: description: "Body object description" required: true schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" responses: "200": description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1602,9 +1580,9 @@ paths: examples: {} delete: tags: - - "api/secure/monitoredcomponent" - description: "Deletes the 'MonitoredComponent' entity with the specified id\ - \ if it exists." + - "api/secure/monitoredtrip" + description: "Deletes the 'MonitoredTrip' entity with the specified id if it\ + \ exists." produces: - "application/json" parameters: @@ -1618,9 +1596,9 @@ paths: description: "Successful operation" examples: {} responseSchema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" schema: - $ref: "#/definitions/MonitoredComponent" + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1641,56 +1619,128 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/user/acceptdependent: + /otp/*: get: tags: - - "api/secure/user" - description: "Accept a dependent request." - parameters: [] + - "otp" + description: "Forwards any GET request to OTP 2. Refer to OTP's\ + \ API documentation for OTP's supported API resources." + produces: + - "application/json" + - "application/xml" + parameters: + - name: "userId" + in: "query" + description: "If a third-party application is making a trip plan request on\ + \ behalf of an end user (OtpUser), the user id must be specified." + required: false + type: "string" responses: "200": - description: "Successful operation" - examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" - schema: - $ref: "#/definitions/OtpUser" - "400": - description: "The request was not formed properly (e.g., some required parameters\ - \ may be missing). See the details of the returned response to determine\ - \ the exact issue." - examples: {} - "401": - description: "The server was not able to authenticate the request. This\ - \ can happen if authentication headers are missing or malformed, or the\ - \ authentication server cannot be reached." - examples: {} - "403": - description: "The requesting user is not allowed to perform the request." - examples: {} - "404": - description: "The requested item was not found." - examples: {} - "500": - description: "An error occurred while performing the request. Contact an\ - \ API administrator for more information." - examples: {} - /api/secure/user/getdependentmobilityprofile: - get: + description: "successful operation" + post: tags: - - "api/secure/user" - description: "Retrieve the mobility profile for each valid dependent user id\ - \ provided." - parameters: [] + - "otp" + description: "Forwards any POST request to OTP 2. Refer to OTP's\ + \ API documentation for OTP's supported API resources." + produces: + - "application/json" + parameters: + - name: "userId" + in: "query" + description: "If a third-party application is making a trip plan request on\ + \ behalf of an end user (OtpUser), the user id must be specified." + required: false + type: "string" responses: "200": - description: "Successful operation" - examples: {} - responseSchema: - $ref: "#/definitions/MobilityProfileLite" - schema: - $ref: "#/definitions/MobilityProfileLite" - "400": + description: "successful operation" + /otp2/*: + get: + tags: + - "otp2" + description: "Forwards any GET request to OTP 2. Refer to OTP's\ + \ API documentation for OTP's supported API resources." + produces: + - "application/json" + - "application/xml" + parameters: + - name: "userId" + in: "query" + description: "If a third-party application is making a trip plan request on\ + \ behalf of an end user (OtpUser), the user id must be specified." + required: false + type: "string" + responses: + "200": + description: "successful operation" + post: + tags: + - "otp2" + description: "Forwards any POST request to OTP 2. Refer to OTP's\ + \ API documentation for OTP's supported API resources." + produces: + - "application/json" + parameters: + - name: "userId" + in: "query" + description: "If a third-party application is making a trip plan request on\ + \ behalf of an end user (OtpUser), the user id must be specified." + required: false + type: "string" + responses: + "200": + description: "successful operation" + /api/secure/user/acceptdependent: + get: + tags: + - "api/secure/user" + description: "Accept a dependent request." + parameters: [] + responses: + "200": + description: "Successful operation" + examples: {} + responseSchema: + $ref: "#/definitions/OtpUser" + schema: + $ref: "#/definitions/OtpUser" + "400": + description: "The request was not formed properly (e.g., some required parameters\ + \ may be missing). See the details of the returned response to determine\ + \ the exact issue." + examples: {} + "401": + description: "The server was not able to authenticate the request. This\ + \ can happen if authentication headers are missing or malformed, or the\ + \ authentication server cannot be reached." + examples: {} + "403": + description: "The requesting user is not allowed to perform the request." + examples: {} + "404": + description: "The requested item was not found." + examples: {} + "500": + description: "An error occurred while performing the request. Contact an\ + \ API administrator for more information." + examples: {} + /api/secure/user/getdependentmobilityprofile: + get: + tags: + - "api/secure/user" + description: "Retrieve the mobility profile for each valid dependent user id\ + \ provided." + parameters: [] + responses: + "200": + description: "Successful operation" + examples: {} + responseSchema: + $ref: "#/definitions/MobilityProfileLite" + schema: + $ref: "#/definitions/MobilityProfileLite" + "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ \ the exact issue." @@ -2046,202 +2096,180 @@ paths: description: "An error occurred while performing the request. Contact an\ \ API administrator for more information." examples: {} - /api/secure/logs: - get: + /api/secure/monitoredtrip/starttracking: + post: tags: - - "api/secure/logs" - description: "Gets a list of all API usage logs." + - "api/secure/monitoredtrip" + description: "Initiates the tracking of a monitored trip." produces: - "application/json" parameters: - - name: "keyId" - in: "query" - description: "If specified, restricts the search to the specified AWS API\ - \ key ID." - required: false - type: "string" - - name: "startDate" - in: "query" - description: "If specified, the earliest date (format yyyy-MM-dd) for which\ - \ usage logs are retrieved." - required: false - type: "string" - default: "30 days prior to the current date" - pattern: "yyyy-MM-dd" - - name: "endDate" - in: "query" - description: "If specified, the latest date (format yyyy-MM-dd) for which\ - \ usage logs are retrieved." - required: false - type: "string" - default: "The current date" - pattern: "yyyy-MM-dd" + - in: "body" + name: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/StartTrackingPayload" responses: "200": description: "successful operation" responseSchema: - type: "array" - items: - $ref: "#/definitions/ApiUsageResult" + $ref: "#/definitions/TrackingResponse" schema: - type: "array" - items: - $ref: "#/definitions/ApiUsageResult" - /api/admin/bugsnag/eventsummary: - get: + $ref: "#/definitions/TrackingResponse" + /api/secure/monitoredtrip/updatetracking: + post: tags: - - "api/admin/bugsnag/eventsummary" - description: "Gets a paginated list of the latest Bugsnag event summaries." + - "api/secure/monitoredtrip" + description: "Provides tracking updates on a monitored trip." produces: - "application/json" parameters: - - name: "limit" - in: "query" - description: "If specified, the maximum number of items to return." - required: false - type: "string" - default: "10" - - name: "offset" - in: "query" - description: "If specified, the number of records to skip/offset." - required: false - type: "string" - default: "0" + - in: "body" + name: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/UpdatedTrackingPayload" responses: "200": description: "successful operation" responseSchema: - type: "array" - items: - $ref: "#/definitions/BugsnagEvent" + $ref: "#/definitions/TrackingResponse" schema: - type: "array" - items: - $ref: "#/definitions/BugsnagEvent" - /api/secure/connected-data: - get: + $ref: "#/definitions/TrackingResponse" + /api/secure/monitoredtrip/track: + post: tags: - - "api/secure/connected-data" - description: "Gets a paginated list of CDP zip files in the configured S3 bucket." + - "api/secure/monitoredtrip" + description: "Starts or updates tracking on a monitored trip." produces: - "application/json" parameters: - - name: "limit" - in: "query" - description: "If specified, the maximum number of items to return." - required: false - type: "string" - default: "10" - - name: "offset" - in: "query" - description: "If specified, the number of records to skip/offset." - required: false - type: "string" - default: "0" + - in: "body" + name: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/TrackPayload" responses: "200": description: "successful operation" responseSchema: - type: "array" - items: - $ref: "#/definitions/CDPFile" + $ref: "#/definitions/TrackingResponse" schema: - type: "array" - items: - $ref: "#/definitions/CDPFile" - /api/secure/connected-data/download: - get: + $ref: "#/definitions/TrackingResponse" + /api/secure/monitoredtrip/endtracking: + post: tags: - - "api/secure/connected-data" - description: "Generates a download link for a specified object within the CDP\ - \ bucket." + - "api/secure/monitoredtrip" + description: "Terminates the tracking of a monitored trip by the user." produces: - "application/json" parameters: - - name: "/download" - in: "query" - description: "The key of the object to generate a link for." + - in: "body" + name: "body" + description: "Body object description" required: true - type: "string" + schema: + $ref: "#/definitions/EndTrackingPayload" responses: "200": description: "successful operation" responseSchema: - type: "array" - items: - $ref: "#/definitions/URL" + $ref: "#/definitions/EndTrackingResponse" schema: - type: "array" - items: - $ref: "#/definitions/URL" - /otp/*: - get: + $ref: "#/definitions/EndTrackingResponse" + /api/secure/monitoredtrip/forciblyendtracking: + post: tags: - - "otp" - description: "Forwards any GET request to OTP 2. Refer to OTP's\ - \ API documentation for OTP's supported API resources." + - "api/secure/monitoredtrip" + description: "Forcibly terminates tracking of a monitored trip by trip ID." produces: - "application/json" - - "application/xml" parameters: - - name: "userId" - in: "query" - description: "If a third-party application is making a trip plan request on\ - \ behalf of an end user (OtpUser), the user id must be specified." - required: false - type: "string" + - in: "body" + name: "body" + description: "Body object description" + required: true + schema: + $ref: "#/definitions/ForceEndTrackingPayload" responses: "200": description: "successful operation" - post: + responseSchema: + $ref: "#/definitions/EndTrackingResponse" + schema: + $ref: "#/definitions/EndTrackingResponse" + /api/secure/triprequests: + get: tags: - - "otp" - description: "Forwards any POST request to OTP 2. Refer to OTP's\ - \ API documentation for OTP's supported API resources." + - "api/secure/triprequests" + description: "Gets a paginated list of the most recent trip requests for a user." produces: - "application/json" parameters: - name: "userId" in: "query" - description: "If a third-party application is making a trip plan request on\ - \ behalf of an end user (OtpUser), the user id must be specified." + description: "The OTP user for which to retrieve trip requests." + required: true + type: "string" + - name: "limit" + in: "query" + description: "If specified, the maximum number of items to return." required: false type: "string" - responses: - "200": - description: "successful operation" - /otp2/*: - get: - tags: - - "otp2" - description: "Forwards any GET request to OTP 2. Refer to OTP's\ - \ API documentation for OTP's supported API resources." - produces: - - "application/json" - - "application/xml" - parameters: - - name: "userId" + default: "10" + - name: "offset" in: "query" - description: "If a third-party application is making a trip plan request on\ - \ behalf of an end user (OtpUser), the user id must be specified." + description: "If specified, the number of records to skip/offset." required: false type: "string" + default: "0" + - name: "fromDate" + in: "query" + description: "If specified, the earliest date (format yyyy-MM-dd) for which\ + \ trip requests are retrieved." + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" + - name: "toDate" + in: "query" + description: "If specified, the latest date (format yyyy-MM-dd) for which\ + \ trip requests are retrieved." + required: false + type: "string" + default: "The current date" + pattern: "yyyy-MM-dd" responses: "200": description: "successful operation" - post: + responseSchema: + $ref: "#/definitions/TripRequest" + schema: + $ref: "#/definitions/TripRequest" + /api/trip-survey/open: + get: tags: - - "otp2" - description: "Forwards any POST request to OTP 2. Refer to OTP's\ - \ API documentation for OTP's supported API resources." - produces: - - "application/json" + - "api/trip-survey" + description: "Generates a tracking survey link for a specified user, trip, notification\ + \ ids." parameters: - - name: "userId" + - name: "user_id" in: "query" - description: "If a third-party application is making a trip plan request on\ - \ behalf of an end user (OtpUser), the user id must be specified." - required: false + description: "The id of the OtpUser that this notification applies to." + required: true + type: "string" + - name: "trip_id" + in: "query" + description: "The id of the MonitoredTrip that this notification applies to." + required: true + type: "string" + - name: "notification_id" + in: "query" + description: "The id of the notification that this notification applies to." + required: true type: "string" responses: "200": @@ -2331,6 +2359,47 @@ definitions: type: "boolean" name: type: "string" + CDPFile: + type: "object" + properties: + key: + type: "string" + name: + type: "string" + size: + type: "integer" + format: "int64" + URL: + type: "object" + properties: + protocol: + type: "string" + host: + type: "string" + port: + type: "integer" + format: "int32" + file: + type: "string" + query: + type: "string" + authority: + type: "string" + path: + type: "string" + userInfo: + type: "string" + ref: + type: "string" + hostAddress: + $ref: "#/definitions/InetAddress" + handler: + $ref: "#/definitions/URLStreamHandler" + hashCode: + type: "integer" + format: "int32" + tempState: + $ref: "#/definitions/UrlDeserializedState" CDPUser: type: "object" properties: @@ -2338,6 +2407,69 @@ definitions: type: "string" S3DownloadTimes: $ref: "#/definitions/Map" + App: + type: "object" + properties: + releaseStage: + type: "string" + BugsnagEvent: + type: "object" + properties: + eventDataId: + type: "string" + projectId: + type: "string" + errorId: + type: "string" + receivedAt: + type: "string" + format: "date" + exceptions: + type: "array" + items: + $ref: "#/definitions/EventException" + severity: + type: "string" + context: + type: "string" + unhandled: + type: "boolean" + app: + $ref: "#/definitions/App" + EventException: + type: "object" + properties: + errorClass: + type: "string" + message: + type: "string" + GetUsageResult: + type: "object" + properties: + usagePlanId: + type: "string" + startDate: + type: "string" + endDate: + type: "string" + position: + type: "string" + items: + $ref: "#/definitions/Map" + ApiUsageResult: + type: "object" + properties: + result: + $ref: "#/definitions/GetUsageResult" + apiUsers: + $ref: "#/definitions/Map" + MonitoredComponent: + type: "object" + properties: + bugsnagProjectId: + type: "string" + name: + type: "string" TripStop: type: "object" properties: @@ -2991,6 +3123,113 @@ definitions: type: "string" address: type: "string" + VerificationResult: + type: "object" + properties: + sid: + type: "string" + status: + type: "string" + valid: + type: "boolean" + UserLocation: + type: "object" + properties: + address: + type: "string" + icon: + type: "string" + lat: + type: "number" + format: "double" + lon: + type: "number" + format: "double" + name: + type: "string" + type: + type: "string" + OtpUser: + type: "object" + properties: + accessibilityRoutingByDefault: + type: "boolean" + hasConsentedToTerms: + type: "boolean" + isPhoneNumberVerified: + type: "boolean" + mobilityProfile: + $ref: "#/definitions/MobilityProfile" + notificationChannel: + type: "array" + items: + type: "string" + enum: + - "EMAIL" + - "PUSH" + - "SMS" + phoneNumber: + type: "string" + smsConsentDate: + type: "string" + format: "date" + preferredLocale: + type: "string" + pushDevices: + type: "integer" + format: "int32" + savedLocations: + type: "array" + items: + $ref: "#/definitions/UserLocation" + storeTripHistory: + type: "boolean" + tripSurveyNotifications: + type: "array" + items: + $ref: "#/definitions/TripSurveyNotification" + applicationId: + type: "string" + relatedUsers: + type: "array" + items: + $ref: "#/definitions/RelatedUser" + dependents: + type: "array" + items: + type: "string" + name: + type: "string" + MobilityProfile: + type: "object" + properties: + isMobilityLimited: + type: "boolean" + mobilityDevices: + type: "array" + items: + type: "string" + mobilityMode: + type: "string" + visionLimitation: + type: "string" + enum: + - "LEGALLY_BLIND" + - "LOW_VISION" + - "NONE" + TripSurveyNotification: + type: "object" + properties: + id: + type: "string" + timeSent: + type: "string" + format: "date" + timeOpened: + type: "string" + format: "date" + journeyId: + type: "string" StartTrackingPayload: type: "object" properties: @@ -3151,214 +3390,6 @@ definitions: type: "string" qualifier: type: "string" - MonitoredComponent: - type: "object" - properties: - bugsnagProjectId: - type: "string" - name: - type: "string" - VerificationResult: - type: "object" - properties: - sid: - type: "string" - status: - type: "string" - valid: - type: "boolean" - UserLocation: - type: "object" - properties: - address: - type: "string" - icon: - type: "string" - lat: - type: "number" - format: "double" - lon: - type: "number" - format: "double" - name: - type: "string" - type: - type: "string" - OtpUser: - type: "object" - properties: - accessibilityRoutingByDefault: - type: "boolean" - hasConsentedToTerms: - type: "boolean" - isPhoneNumberVerified: - type: "boolean" - mobilityProfile: - $ref: "#/definitions/MobilityProfile" - notificationChannel: - type: "array" - items: - type: "string" - enum: - - "EMAIL" - - "PUSH" - - "SMS" - phoneNumber: - type: "string" - smsConsentDate: - type: "string" - format: "date" - preferredLocale: - type: "string" - pushDevices: - type: "integer" - format: "int32" - savedLocations: - type: "array" - items: - $ref: "#/definitions/UserLocation" - storeTripHistory: - type: "boolean" - tripSurveyNotifications: - type: "array" - items: - $ref: "#/definitions/TripSurveyNotification" - applicationId: - type: "string" - relatedUsers: - type: "array" - items: - $ref: "#/definitions/RelatedUser" - dependents: - type: "array" - items: - type: "string" - name: - type: "string" - MobilityProfile: - type: "object" - properties: - isMobilityLimited: - type: "boolean" - mobilityDevices: - type: "array" - items: - type: "string" - mobilityMode: - type: "string" - visionLimitation: - type: "string" - enum: - - "LEGALLY_BLIND" - - "LOW_VISION" - - "NONE" - TripSurveyNotification: - type: "object" - properties: - id: - type: "string" - timeSent: - type: "string" - format: "date" - journeyId: - type: "string" - GetUsageResult: - type: "object" - properties: - usagePlanId: - type: "string" - startDate: - type: "string" - endDate: - type: "string" - position: - type: "string" - items: - $ref: "#/definitions/Map" - ApiUsageResult: - type: "object" - properties: - result: - $ref: "#/definitions/GetUsageResult" - apiUsers: - $ref: "#/definitions/Map" - App: - type: "object" - properties: - releaseStage: - type: "string" - BugsnagEvent: - type: "object" - properties: - eventDataId: - type: "string" - projectId: - type: "string" - errorId: - type: "string" - receivedAt: - type: "string" - format: "date" - exceptions: - type: "array" - items: - $ref: "#/definitions/EventException" - severity: - type: "string" - context: - type: "string" - unhandled: - type: "boolean" - app: - $ref: "#/definitions/App" - EventException: - type: "object" - properties: - errorClass: - type: "string" - message: - type: "string" - CDPFile: - type: "object" - properties: - key: - type: "string" - name: - type: "string" - size: - type: "integer" - format: "int64" - URL: - type: "object" - properties: - protocol: - type: "string" - host: - type: "string" - port: - type: "integer" - format: "int32" - file: - type: "string" - query: - type: "string" - authority: - type: "string" - path: - type: "string" - userInfo: - type: "string" - ref: - type: "string" - hostAddress: - $ref: "#/definitions/InetAddress" - handler: - $ref: "#/definitions/URLStreamHandler" - hashCode: - type: "integer" - format: "int32" - tempState: - $ref: "#/definitions/UrlDeserializedState" externalDocs: description: "" url: "" diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/TripSurveyControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/TripSurveyControllerTest.java new file mode 100644 index 000000000..31a963c59 --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/TripSurveyControllerTest.java @@ -0,0 +1,171 @@ +package org.opentripplanner.middleware.controllers.api; + +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.models.MonitoredTrip; +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.models.TripSurveyNotification; +import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.testutils.ApiTestUtils; +import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; +import org.opentripplanner.middleware.testutils.PersistenceTestUtils; +import org.opentripplanner.middleware.utils.HttpResponseValues; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.opentripplanner.middleware.testutils.ApiTestUtils.makeRequest; + +class TripSurveyControllerTest extends OtpMiddlewareTestEnvironment { + public static final String SURVEY_PATH_TEMPLATE = "api/trip-survey/open?user_id=%s&trip_id=%s¬ification_id=%s"; + private static OtpUser otpUser; + private static MonitoredTrip monitoredTrip; + private static TrackedJourney trackedJourney; + private static final String NOTIFICATION_ID = UUID.randomUUID().toString(); + + @BeforeAll + public static void setUp() throws Exception { + assumeTrue(IS_END_TO_END); + otpUser = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("test-otpuser")); + monitoredTrip = createMonitoredTrip(); + + trackedJourney = new TrackedJourney(); + trackedJourney.id = UUID.randomUUID().toString(); + Persistence.trackedJourneys.create(trackedJourney); + } + + private static MonitoredTrip createMonitoredTrip() { + MonitoredTrip trip = new MonitoredTrip(); + trip.id = UUID.randomUUID().toString(); + trip.userId = otpUser.id; + Persistence.monitoredTrips.create(trip); + return trip; + } + + @AfterAll + public static void tearDown() throws Exception { + assumeTrue(IS_END_TO_END); + otpUser = Persistence.otpUsers.getById(otpUser.id); + if (otpUser != null) otpUser.delete(true); + monitoredTrip = Persistence.monitoredTrips.getById(monitoredTrip.id); + if (monitoredTrip != null) monitoredTrip.delete(); + trackedJourney = Persistence.trackedJourneys.getById(trackedJourney.id); + if (trackedJourney != null) trackedJourney.delete(); + } + + @BeforeEach + void setUpTest() { + otpUser.tripSurveyNotifications = List.of( + new TripSurveyNotification("other-notification", Date.from(Instant.now()), "other-journey"), + new TripSurveyNotification(NOTIFICATION_ID, Date.from(Instant.now()), trackedJourney.id) + ); + Persistence.otpUsers.replace(otpUser.id, otpUser); + } + + @Test + void canMakeTripSurveyUrl() { + assertEquals( + "https://subdomain.typeform.com/to/survey-1#user_id=user-2&trip_id=trip-3¬ification_id=notif-4", + TripSurveyController.makeTripSurveyUrl("subdomain", "survey-1", "user-2", "trip-3", "notif-4") + ); + } + + @Test + void canOpenSurveyAndUpdateNotificationStatus() { + assumeTrue(IS_END_TO_END); + + OtpUser existingUser = Persistence.otpUsers.getById(otpUser.id); + assertNotNull(existingUser); + existingUser.tripSurveyNotifications.forEach(n -> assertNull(n.timeOpened)); + + Instant requestInstant = Instant.now(); + var response = invokeSurveyEndpoint(otpUser.id, monitoredTrip.id, NOTIFICATION_ID); + Instant requestCompleteInstant = Instant.now(); + + assertEquals(HttpStatus.OK_200, response.status); + + OtpUser updatedUser = Persistence.otpUsers.getById(otpUser.id); + assertNotNull(updatedUser); + assertEquals(otpUser.tripSurveyNotifications.size(), updatedUser.tripSurveyNotifications.size()); + + int updatedNotificationCount = 0; + Date notificationTimeOpened = null; + for (TripSurveyNotification notification : updatedUser.tripSurveyNotifications) { + if (NOTIFICATION_ID.equals(notification.id)) { + notificationTimeOpened = notification.timeOpened; + assertNotNull(notificationTimeOpened); + assertTrue(notificationTimeOpened.toInstant().isAfter(requestInstant)); + assertTrue(notificationTimeOpened.toInstant().isBefore(requestCompleteInstant)); + updatedNotificationCount++; + } else { + assertNull(notification.timeOpened); + } + } + assertEquals(1, updatedNotificationCount); + assertNotNull(notificationTimeOpened); + + // A second request should not change the notification opened date. + var response2 = invokeSurveyEndpoint(otpUser.id, monitoredTrip.id, NOTIFICATION_ID); + assertEquals(HttpStatus.OK_200, response2.status); + + updatedUser = Persistence.otpUsers.getById(otpUser.id); + assertNotNull(updatedUser); + for (TripSurveyNotification notification : updatedUser.tripSurveyNotifications) { + if (NOTIFICATION_ID.equals(notification.id)) { + assertEquals(notificationTimeOpened, notification.timeOpened); + } + } + } + + @ParameterizedTest + @MethodSource("createShouldRejectInvalidParamsCases") + void shouldRejectInvalidParams(String userId, String tripId, String notificationId) { + assumeTrue(IS_END_TO_END); + + var response = invokeSurveyEndpoint(userId, tripId, notificationId); + + assertEquals( + HttpStatus.BAD_REQUEST_400, + response.status, + "Invalid URL params should result in HTTP Status 400." + ); + } + + private static Stream createShouldRejectInvalidParamsCases() { + return Stream.of( + Arguments.of("invalid-user-id", monitoredTrip.id, NOTIFICATION_ID), + Arguments.of(null, monitoredTrip.id, NOTIFICATION_ID), + Arguments.of(otpUser.id, "invalid-trip-id", NOTIFICATION_ID), + Arguments.of(otpUser.id, null, NOTIFICATION_ID), + Arguments.of(otpUser.id, monitoredTrip.id, "invalid-notification-id"), + Arguments.of(otpUser.id, monitoredTrip.id, null) + ); + } + + /** Helper to invoke the survey endpoint */ + private static HttpResponseValues invokeSurveyEndpoint(String userId, String tripId, String notificationId) { + return makeRequest( + String.format(SURVEY_PATH_TEMPLATE, userId, tripId, notificationId), + "", + Map.of(), + HttpMethod.GET + ); + } +}