1use aide::{OperationIo, transform::TransformOperation};
8use axum::{Json, response::IntoResponse};
9use axum_extra::extract::{Query, QueryRejection};
10use axum_macros::FromRequestParts;
11use hyper::StatusCode;
12use mas_axum_utils::record_error;
13use mas_storage::{Page, upstream_oauth2::UpstreamOAuthLinkFilter};
14use schemars::JsonSchema;
15use serde::Deserialize;
16use ulid::Ulid;
17
18use crate::{
19 admin::{
20 call_context::CallContext,
21 model::{Resource, UpstreamOAuthLink},
22 params::{IncludeCount, Pagination},
23 response::{ErrorResponse, PaginatedResponse},
24 },
25 impl_from_error_for_route,
26};
27
28#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)]
29#[serde(rename = "UpstreamOAuthLinkFilter")]
30#[aide(input_with = "Query<FilterParams>")]
31#[from_request(via(Query), rejection(RouteError))]
32pub struct FilterParams {
33 #[serde(rename = "filter[user]")]
35 #[schemars(with = "Option<crate::admin::schema::Ulid>")]
36 user: Option<Ulid>,
37
38 #[serde(rename = "filter[provider]")]
40 #[schemars(with = "Option<crate::admin::schema::Ulid>")]
41 provider: Option<Ulid>,
42
43 #[serde(rename = "filter[subject]")]
45 subject: Option<String>,
46
47 #[serde(rename = "filter[human-account-name]")]
50 human_account_name: Option<String>,
51}
52
53impl std::fmt::Display for FilterParams {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 let mut sep = '?';
56
57 if let Some(user) = self.user {
58 write!(f, "{sep}filter[user]={user}")?;
59 sep = '&';
60 }
61
62 if let Some(provider) = self.provider {
63 write!(f, "{sep}filter[provider]={provider}")?;
64 sep = '&';
65 }
66
67 if let Some(subject) = &self.subject {
68 write!(f, "{sep}filter[subject]={subject}")?;
69 sep = '&';
70 }
71
72 if let Some(human_account_name) = &self.human_account_name {
73 write!(f, "{sep}filter[human-account-name]={human_account_name}")?;
74 sep = '&';
75 }
76
77 let _ = sep;
78 Ok(())
79 }
80}
81
82#[derive(Debug, thiserror::Error, OperationIo)]
83#[aide(output_with = "Json<ErrorResponse>")]
84pub enum RouteError {
85 #[error(transparent)]
86 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
87
88 #[error("User ID {0} not found")]
89 UserNotFound(Ulid),
90
91 #[error("Provider ID {0} not found")]
92 ProviderNotFound(Ulid),
93
94 #[error("Invalid filter parameters")]
95 InvalidFilter(#[from] QueryRejection),
96}
97
98impl_from_error_for_route!(mas_storage::RepositoryError);
99
100impl IntoResponse for RouteError {
101 fn into_response(self) -> axum::response::Response {
102 let error = ErrorResponse::from_error(&self);
103 let sentry_event_id = record_error!(self, Self::Internal(_));
104 let status = match self {
105 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
106 Self::UserNotFound(_) | Self::ProviderNotFound(_) => StatusCode::NOT_FOUND,
107 Self::InvalidFilter(_) => StatusCode::BAD_REQUEST,
108 };
109 (status, sentry_event_id, Json(error)).into_response()
110 }
111}
112
113pub fn doc(operation: TransformOperation) -> TransformOperation {
114 operation
115 .id("listUpstreamOAuthLinks")
116 .summary("List upstream OAuth 2.0 links")
117 .description("Retrieve a list of upstream OAuth 2.0 links.")
118 .tag("upstream-oauth-link")
119 .response_with::<200, Json<PaginatedResponse<UpstreamOAuthLink>>, _>(|t| {
120 let links = UpstreamOAuthLink::samples();
121 let pagination = mas_storage::Pagination::first(links.len());
122 let page = Page {
123 edges: links
124 .into_iter()
125 .map(|node| mas_storage::pagination::Edge {
126 cursor: node.id(),
127 node,
128 })
129 .collect(),
130 has_next_page: true,
131 has_previous_page: false,
132 };
133
134 t.description("Paginated response of upstream OAuth 2.0 links")
135 .example(PaginatedResponse::for_page(
136 page,
137 pagination,
138 Some(42),
139 UpstreamOAuthLink::PATH,
140 ))
141 })
142 .response_with::<404, RouteError, _>(|t| {
143 let response = ErrorResponse::from_error(&RouteError::UserNotFound(Ulid::nil()));
144 t.description("User or provider was not found")
145 .example(response)
146 })
147}
148
149#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.list", skip_all)]
150pub async fn handler(
151 CallContext { mut repo, .. }: CallContext,
152 Pagination(pagination, include_count): Pagination,
153 params: FilterParams,
154) -> Result<Json<PaginatedResponse<UpstreamOAuthLink>>, RouteError> {
155 let base = format!("{path}{params}", path = UpstreamOAuthLink::PATH);
156 let base = include_count.add_to_base(&base);
157 let filter = UpstreamOAuthLinkFilter::default();
158
159 let maybe_user = if let Some(user_id) = params.user {
161 let user = repo
162 .user()
163 .lookup(user_id)
164 .await?
165 .ok_or(RouteError::UserNotFound(user_id))?;
166 Some(user)
167 } else {
168 None
169 };
170
171 let filter = if let Some(user) = &maybe_user {
172 filter.for_user(user)
173 } else {
174 filter
175 };
176
177 let maybe_provider = if let Some(provider_id) = params.provider {
179 let provider = repo
180 .upstream_oauth_provider()
181 .lookup(provider_id)
182 .await?
183 .ok_or(RouteError::ProviderNotFound(provider_id))?;
184 Some(provider)
185 } else {
186 None
187 };
188
189 let filter = if let Some(provider) = &maybe_provider {
190 filter.for_provider(provider)
191 } else {
192 filter
193 };
194
195 let filter = if let Some(subject) = ¶ms.subject {
196 filter.for_subject(subject)
197 } else {
198 filter
199 };
200
201 let filter = if let Some(name) = ¶ms.human_account_name {
202 filter.matching_human_account_name(name)
203 } else {
204 filter
205 };
206
207 let response = match include_count {
208 IncludeCount::True => {
209 let page = repo
210 .upstream_oauth_link()
211 .list(filter, pagination)
212 .await?
213 .map(UpstreamOAuthLink::from);
214 let count = repo.upstream_oauth_link().count(filter).await?;
215 PaginatedResponse::for_page(page, pagination, Some(count), &base)
216 }
217 IncludeCount::False => {
218 let page = repo
219 .upstream_oauth_link()
220 .list(filter, pagination)
221 .await?
222 .map(UpstreamOAuthLink::from);
223 PaginatedResponse::for_page(page, pagination, None, &base)
224 }
225 IncludeCount::Only => {
226 let count = repo.upstream_oauth_link().count(filter).await?;
227 PaginatedResponse::for_count_only(count, &base)
228 }
229 };
230
231 Ok(Json(response))
232}
233
234#[cfg(test)]
235mod tests {
236 use hyper::{Request, StatusCode};
237 use insta::assert_json_snapshot;
238 use sqlx::PgPool;
239
240 use super::super::test_utils;
241 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
242
243 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
244 async fn test_list(pool: PgPool) {
245 setup();
246 let mut state = TestState::from_pool(pool).await.unwrap();
247 let token = state.token_with_scope("urn:mas:admin").await;
248 let mut rng = state.rng();
249
250 let mut repo = state.repository().await.unwrap();
252 let alice = repo
253 .user()
254 .add(&mut rng, &state.clock, "alice".to_owned())
255 .await
256 .unwrap();
257 let bob = repo
258 .user()
259 .add(&mut rng, &state.clock, "bob".to_owned())
260 .await
261 .unwrap();
262 let provider1 = repo
263 .upstream_oauth_provider()
264 .add(
265 &mut rng,
266 &state.clock,
267 test_utils::oidc_provider_params("acme"),
268 )
269 .await
270 .unwrap();
271 let provider2 = repo
272 .upstream_oauth_provider()
273 .add(
274 &mut rng,
275 &state.clock,
276 test_utils::oidc_provider_params("example"),
277 )
278 .await
279 .unwrap();
280
281 let link1 = repo
283 .upstream_oauth_link()
284 .add(
285 &mut rng,
286 &state.clock,
287 &provider1,
288 "subject1".to_owned(),
289 Some("alice@acme".to_owned()),
290 )
291 .await
292 .unwrap();
293 repo.upstream_oauth_link()
294 .associate_to_user(&link1, &alice)
295 .await
296 .unwrap();
297 let link2 = repo
298 .upstream_oauth_link()
299 .add(
300 &mut rng,
301 &state.clock,
302 &provider2,
303 "subject2".to_owned(),
304 Some("alice@example".to_owned()),
305 )
306 .await
307 .unwrap();
308 repo.upstream_oauth_link()
309 .associate_to_user(&link2, &alice)
310 .await
311 .unwrap();
312 let link3 = repo
313 .upstream_oauth_link()
314 .add(
315 &mut rng,
316 &state.clock,
317 &provider1,
318 "subject3".to_owned(),
319 Some("bob@acme".to_owned()),
320 )
321 .await
322 .unwrap();
323 repo.upstream_oauth_link()
324 .associate_to_user(&link3, &bob)
325 .await
326 .unwrap();
327
328 repo.save().await.unwrap();
329
330 let request = Request::get("/api/admin/v1/upstream-oauth-links")
331 .bearer(&token)
332 .empty();
333 let response = state.request(request).await;
334 response.assert_status(StatusCode::OK);
335 let body: serde_json::Value = response.json();
336 assert_json_snapshot!(body, @r#"
337 {
338 "meta": {
339 "count": 3
340 },
341 "data": [
342 {
343 "type": "upstream-oauth-link",
344 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
345 "attributes": {
346 "created_at": "2022-01-16T14:40:00Z",
347 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
348 "subject": "subject1",
349 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
350 "human_account_name": "alice@acme"
351 },
352 "links": {
353 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
354 },
355 "meta": {
356 "page": {
357 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
358 }
359 }
360 },
361 {
362 "type": "upstream-oauth-link",
363 "id": "01FSHN9AG0PJZ6DZNTAA1XKPT4",
364 "attributes": {
365 "created_at": "2022-01-16T14:40:00Z",
366 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
367 "subject": "subject3",
368 "user_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4",
369 "human_account_name": "bob@acme"
370 },
371 "links": {
372 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4"
373 },
374 "meta": {
375 "page": {
376 "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4"
377 }
378 }
379 },
380 {
381 "type": "upstream-oauth-link",
382 "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
383 "attributes": {
384 "created_at": "2022-01-16T14:40:00Z",
385 "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
386 "subject": "subject2",
387 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
388 "human_account_name": "alice@example"
389 },
390 "links": {
391 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
392 },
393 "meta": {
394 "page": {
395 "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
396 }
397 }
398 }
399 ],
400 "links": {
401 "self": "/api/admin/v1/upstream-oauth-links?page[first]=10",
402 "first": "/api/admin/v1/upstream-oauth-links?page[first]=10",
403 "last": "/api/admin/v1/upstream-oauth-links?page[last]=10"
404 }
405 }
406 "#);
407
408 let request = Request::get(format!(
410 "/api/admin/v1/upstream-oauth-links?filter[user]={}",
411 alice.id
412 ))
413 .bearer(&token)
414 .empty();
415
416 let response = state.request(request).await;
417 response.assert_status(StatusCode::OK);
418 let body: serde_json::Value = response.json();
419 assert_json_snapshot!(body, @r#"
420 {
421 "meta": {
422 "count": 2
423 },
424 "data": [
425 {
426 "type": "upstream-oauth-link",
427 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
428 "attributes": {
429 "created_at": "2022-01-16T14:40:00Z",
430 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
431 "subject": "subject1",
432 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
433 "human_account_name": "alice@acme"
434 },
435 "links": {
436 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
437 },
438 "meta": {
439 "page": {
440 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
441 }
442 }
443 },
444 {
445 "type": "upstream-oauth-link",
446 "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
447 "attributes": {
448 "created_at": "2022-01-16T14:40:00Z",
449 "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
450 "subject": "subject2",
451 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
452 "human_account_name": "alice@example"
453 },
454 "links": {
455 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
456 },
457 "meta": {
458 "page": {
459 "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
460 }
461 }
462 }
463 ],
464 "links": {
465 "self": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[first]=10",
466 "first": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[first]=10",
467 "last": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&page[last]=10"
468 }
469 }
470 "#);
471
472 let request = Request::get(format!(
474 "/api/admin/v1/upstream-oauth-links?filter[provider]={}",
475 provider1.id
476 ))
477 .bearer(&token)
478 .empty();
479
480 let response = state.request(request).await;
481 response.assert_status(StatusCode::OK);
482 let body: serde_json::Value = response.json();
483 assert_json_snapshot!(body, @r#"
484 {
485 "meta": {
486 "count": 2
487 },
488 "data": [
489 {
490 "type": "upstream-oauth-link",
491 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
492 "attributes": {
493 "created_at": "2022-01-16T14:40:00Z",
494 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
495 "subject": "subject1",
496 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
497 "human_account_name": "alice@acme"
498 },
499 "links": {
500 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
501 },
502 "meta": {
503 "page": {
504 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
505 }
506 }
507 },
508 {
509 "type": "upstream-oauth-link",
510 "id": "01FSHN9AG0PJZ6DZNTAA1XKPT4",
511 "attributes": {
512 "created_at": "2022-01-16T14:40:00Z",
513 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
514 "subject": "subject3",
515 "user_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4",
516 "human_account_name": "bob@acme"
517 },
518 "links": {
519 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4"
520 },
521 "meta": {
522 "page": {
523 "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4"
524 }
525 }
526 }
527 ],
528 "links": {
529 "self": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&page[first]=10",
530 "first": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&page[first]=10",
531 "last": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&page[last]=10"
532 }
533 }
534 "#);
535
536 let request = Request::get(format!(
538 "/api/admin/v1/upstream-oauth-links?filter[subject]={}",
539 "subject1"
540 ))
541 .bearer(&token)
542 .empty();
543
544 let response = state.request(request).await;
545 response.assert_status(StatusCode::OK);
546 let body: serde_json::Value = response.json();
547 assert_json_snapshot!(body, @r#"
548 {
549 "meta": {
550 "count": 1
551 },
552 "data": [
553 {
554 "type": "upstream-oauth-link",
555 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
556 "attributes": {
557 "created_at": "2022-01-16T14:40:00Z",
558 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
559 "subject": "subject1",
560 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
561 "human_account_name": "alice@acme"
562 },
563 "links": {
564 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
565 },
566 "meta": {
567 "page": {
568 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
569 }
570 }
571 }
572 ],
573 "links": {
574 "self": "/api/admin/v1/upstream-oauth-links?filter[subject]=subject1&page[first]=10",
575 "first": "/api/admin/v1/upstream-oauth-links?filter[subject]=subject1&page[first]=10",
576 "last": "/api/admin/v1/upstream-oauth-links?filter[subject]=subject1&page[last]=10"
577 }
578 }
579 "#);
580
581 let request =
583 Request::get("/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE%40")
584 .bearer(&token)
585 .empty();
586
587 let response = state.request(request).await;
588 response.assert_status(StatusCode::OK);
589 let body: serde_json::Value = response.json();
590 assert_json_snapshot!(body, @r#"
591 {
592 "meta": {
593 "count": 2
594 },
595 "data": [
596 {
597 "type": "upstream-oauth-link",
598 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
599 "attributes": {
600 "created_at": "2022-01-16T14:40:00Z",
601 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
602 "subject": "subject1",
603 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
604 "human_account_name": "alice@acme"
605 },
606 "links": {
607 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
608 },
609 "meta": {
610 "page": {
611 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
612 }
613 }
614 },
615 {
616 "type": "upstream-oauth-link",
617 "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
618 "attributes": {
619 "created_at": "2022-01-16T14:40:00Z",
620 "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
621 "subject": "subject2",
622 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
623 "human_account_name": "alice@example"
624 },
625 "links": {
626 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
627 },
628 "meta": {
629 "page": {
630 "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
631 }
632 }
633 }
634 ],
635 "links": {
636 "self": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[first]=10",
637 "first": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[first]=10",
638 "last": "/api/admin/v1/upstream-oauth-links?filter[human-account-name]=ALICE@&page[last]=10"
639 }
640 }
641 "#);
642
643 let request = Request::get("/api/admin/v1/upstream-oauth-links?count=false")
645 .bearer(&token)
646 .empty();
647 let response = state.request(request).await;
648 response.assert_status(StatusCode::OK);
649 let body: serde_json::Value = response.json();
650 assert_json_snapshot!(body, @r#"
651 {
652 "data": [
653 {
654 "type": "upstream-oauth-link",
655 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
656 "attributes": {
657 "created_at": "2022-01-16T14:40:00Z",
658 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
659 "subject": "subject1",
660 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
661 "human_account_name": "alice@acme"
662 },
663 "links": {
664 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
665 },
666 "meta": {
667 "page": {
668 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
669 }
670 }
671 },
672 {
673 "type": "upstream-oauth-link",
674 "id": "01FSHN9AG0PJZ6DZNTAA1XKPT4",
675 "attributes": {
676 "created_at": "2022-01-16T14:40:00Z",
677 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
678 "subject": "subject3",
679 "user_id": "01FSHN9AG0AJ6AC5HQ9X6H4RP4",
680 "human_account_name": "bob@acme"
681 },
682 "links": {
683 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0PJZ6DZNTAA1XKPT4"
684 },
685 "meta": {
686 "page": {
687 "cursor": "01FSHN9AG0PJZ6DZNTAA1XKPT4"
688 }
689 }
690 },
691 {
692 "type": "upstream-oauth-link",
693 "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
694 "attributes": {
695 "created_at": "2022-01-16T14:40:00Z",
696 "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
697 "subject": "subject2",
698 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
699 "human_account_name": "alice@example"
700 },
701 "links": {
702 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
703 },
704 "meta": {
705 "page": {
706 "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
707 }
708 }
709 }
710 ],
711 "links": {
712 "self": "/api/admin/v1/upstream-oauth-links?count=false&page[first]=10",
713 "first": "/api/admin/v1/upstream-oauth-links?count=false&page[first]=10",
714 "last": "/api/admin/v1/upstream-oauth-links?count=false&page[last]=10"
715 }
716 }
717 "#);
718
719 let request = Request::get("/api/admin/v1/upstream-oauth-links?count=only")
721 .bearer(&token)
722 .empty();
723 let response = state.request(request).await;
724 response.assert_status(StatusCode::OK);
725 let body: serde_json::Value = response.json();
726 assert_json_snapshot!(body, @r###"
727 {
728 "meta": {
729 "count": 3
730 },
731 "links": {
732 "self": "/api/admin/v1/upstream-oauth-links?count=only"
733 }
734 }
735 "###);
736
737 let request = Request::get(format!(
739 "/api/admin/v1/upstream-oauth-links?count=false&filter[user]={}",
740 alice.id
741 ))
742 .bearer(&token)
743 .empty();
744 let response = state.request(request).await;
745 response.assert_status(StatusCode::OK);
746 let body: serde_json::Value = response.json();
747 assert_json_snapshot!(body, @r#"
748 {
749 "data": [
750 {
751 "type": "upstream-oauth-link",
752 "id": "01FSHN9AG0AQZQP8DX40GD59PW",
753 "attributes": {
754 "created_at": "2022-01-16T14:40:00Z",
755 "provider_id": "01FSHN9AG09NMZYX8MFYH578R9",
756 "subject": "subject1",
757 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
758 "human_account_name": "alice@acme"
759 },
760 "links": {
761 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0AQZQP8DX40GD59PW"
762 },
763 "meta": {
764 "page": {
765 "cursor": "01FSHN9AG0AQZQP8DX40GD59PW"
766 }
767 }
768 },
769 {
770 "type": "upstream-oauth-link",
771 "id": "01FSHN9AG0QHEHKX2JNQ2A2D07",
772 "attributes": {
773 "created_at": "2022-01-16T14:40:00Z",
774 "provider_id": "01FSHN9AG0KEPHYQQXW9XPTX6Z",
775 "subject": "subject2",
776 "user_id": "01FSHN9AG0MZAA6S4AF7CTV32E",
777 "human_account_name": "alice@example"
778 },
779 "links": {
780 "self": "/api/admin/v1/upstream-oauth-links/01FSHN9AG0QHEHKX2JNQ2A2D07"
781 },
782 "meta": {
783 "page": {
784 "cursor": "01FSHN9AG0QHEHKX2JNQ2A2D07"
785 }
786 }
787 }
788 ],
789 "links": {
790 "self": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10",
791 "first": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[first]=10",
792 "last": "/api/admin/v1/upstream-oauth-links?filter[user]=01FSHN9AG0MZAA6S4AF7CTV32E&count=false&page[last]=10"
793 }
794 }
795 "#);
796
797 let request = Request::get(format!(
799 "/api/admin/v1/upstream-oauth-links?count=only&filter[provider]={}",
800 provider1.id
801 ))
802 .bearer(&token)
803 .empty();
804 let response = state.request(request).await;
805 response.assert_status(StatusCode::OK);
806 let body: serde_json::Value = response.json();
807 assert_json_snapshot!(body, @r#"
808 {
809 "meta": {
810 "count": 2
811 },
812 "links": {
813 "self": "/api/admin/v1/upstream-oauth-links?filter[provider]=01FSHN9AG09NMZYX8MFYH578R9&count=only"
814 }
815 }
816 "#);
817 }
818}