1#![cfg_attr(test, recursion_limit = "512")]
209#![cfg_attr(docsrs, feature(doc_cfg))]
210
211mod api;
212mod body;
213mod error;
214mod from_response;
215mod page;
216
217pub mod auth;
218pub mod etag;
219pub mod models;
220pub mod params;
221pub mod service;
222
223use api::repos::RepoRef;
224use api::users::UserRef;
225use body::OctoBody;
226use chrono::{DateTime, Utc};
227use http::{HeaderMap, HeaderValue, Method, Uri};
228use http_body_util::combinators::BoxBody;
229use http_body_util::BodyExt;
230use service::middleware::auth_header::AuthHeaderLayer;
231use std::convert::{Infallible, TryInto};
232use std::fmt;
233use std::future::Future;
234use std::io::Write;
235use std::marker::PhantomData;
236use std::pin::Pin;
237use std::str::FromStr;
238use std::sync::{Arc, RwLock};
239use web_time::Duration;
240
241use http::{header::HeaderName, StatusCode};
242use hyper::{Request, Response};
243
244use once_cell::sync::Lazy;
245use secrecy::{ExposeSecret, SecretString};
246use serde::{Deserialize, Serialize};
247use snafu::*;
248use tower::{buffer::Buffer, util::BoxService, BoxError, Layer, Service, ServiceExt};
249
250use bytes::Bytes;
251use http::header::USER_AGENT;
252use http::request::Builder;
253#[cfg(feature = "opentls")]
254use hyper_tls::HttpsConnector;
255
256#[cfg(feature = "rustls")]
257use hyper_rustls::HttpsConnectorBuilder;
258
259#[cfg(feature = "retry")]
260use tower::retry::{Retry, RetryLayer};
261
262#[cfg(feature = "timeout")]
263use hyper_timeout::TimeoutConnector;
264
265use tower_http::{classify::ServerErrorsFailureClass, map_response_body::MapResponseBodyLayer};
266
267#[cfg(feature = "tracing")]
268use {tower_http::trace::TraceLayer, tracing::Span};
269
270use crate::error::{
271 HttpSnafu, HyperSnafu, InvalidUtf8Snafu, SerdeSnafu, SerdeUrlEncodedSnafu, ServiceSnafu,
272 UriParseError, UriParseSnafu, UriSnafu,
273};
274
275use crate::service::middleware::base_uri::BaseUriLayer;
276use crate::service::middleware::extra_headers::ExtraHeadersLayer;
277
278#[cfg(feature = "retry")]
279use crate::service::middleware::retry::RetryConfig;
280
281use auth::{AppAuth, Auth};
282use models::{AppId, InstallationId, InstallationToken, RepositoryId, UserId};
283
284pub use self::{
285 api::{
286 actions, activity, apps, checks, code_scannings, commits, current, events, gists,
287 gitignore, hooks, issues, licenses, markdown, orgs, projects, pulls, ratelimit, repos,
288 search, teams, users, workflows,
289 },
290 error::{Error, GitHubError},
291 from_response::FromResponse,
292 page::Page,
293};
294
295pub type Result<T, E = error::Error> = std::result::Result<T, E>;
297
298const GITHUB_BASE_URI: &str = "https://api.github.com";
299const GITHUB_BASE_UPLOAD_URI: &str = "https://uploads.github.com";
300
301#[cfg(feature = "default-client")]
302static STATIC_INSTANCE: Lazy<arc_swap::ArcSwap<Octocrab>> =
303 Lazy::new(|| arc_swap::ArcSwap::from_pointee(Octocrab::default()));
304
305pub fn format_preview(preview: impl AsRef<str>) -> String {
311 format!("application/vnd.github.{}-preview", preview.as_ref())
312}
313
314pub fn format_media_type(media_type: impl AsRef<str>) -> String {
322 let media_type = media_type.as_ref();
323 let json_suffix = match media_type {
324 "raw" | "text" | "html" | "full" => "+json",
325 _ => "",
326 };
327
328 format!("application/vnd.github.v3.{media_type}{json_suffix}")
329}
330
331#[derive(Debug, Deserialize)]
332struct GitHubErrorBody {
333 pub documentation_url: Option<String>,
334 pub errors: Option<Vec<serde_json::Value>>,
335 pub message: String,
336}
337
338pub async fn map_github_error(
341 response: http::Response<BoxBody<Bytes, crate::Error>>,
342) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
343 if response.status().is_success() {
344 Ok(response)
345 } else {
346 let (parts, body) = response.into_parts();
347 let GitHubErrorBody {
348 documentation_url,
349 errors,
350 message,
351 } = serde_json::from_slice(body.collect().await?.to_bytes().as_ref())
352 .context(error::SerdeSnafu)?;
353
354 Err(error::Error::GitHub {
355 source: Box::new(GitHubError {
356 status_code: parts.status,
357 documentation_url,
358 errors,
359 message,
360 }),
361 backtrace: Backtrace::capture(),
362 })
363 }
364}
365
366#[cfg(feature = "default-client")]
376#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
377pub fn initialise(crab: Octocrab) -> Arc<Octocrab> {
378 STATIC_INSTANCE.swap(Arc::from(crab))
379}
380
381#[cfg(feature = "default-client")]
390#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
391pub fn instance() -> Arc<Octocrab> {
392 STATIC_INSTANCE.load().clone()
393}
394
395type Executor = Box<dyn Fn(Pin<Box<dyn Future<Output = ()>>>)>;
396
397pub struct OctocrabBuilder<Svc, Config, Auth, LayerReady> {
413 service: Svc,
414 auth: Auth,
415 config: Config,
416 _layer_ready: PhantomData<LayerReady>,
417 executor: Option<Executor>,
418}
419
420pub struct NoConfig {}
422
423pub struct NoSvc {}
425
426pub struct NotLayerReady {}
428pub struct LayerReady {}
429
430pub struct NoAuth {}
432
433impl OctocrabBuilder<NoSvc, NoConfig, NoAuth, NotLayerReady> {
434 pub fn new_empty() -> Self {
435 OctocrabBuilder {
436 service: NoSvc {},
437 auth: NoAuth {},
438 config: NoConfig {},
439 _layer_ready: PhantomData,
440 executor: None,
441 }
442 }
443}
444
445impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
446 pub fn new() -> Self {
447 OctocrabBuilder::default()
448 }
449}
450
451impl<Config, Auth> OctocrabBuilder<NoSvc, Config, Auth, NotLayerReady> {
452 pub fn with_service<Svc>(self, service: Svc) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
453 OctocrabBuilder {
454 service,
455 auth: self.auth,
456 config: self.config,
457 _layer_ready: PhantomData,
458 executor: None,
459 }
460 }
461}
462
463impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
464where
465 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
466 Svc::Future: Send + 'static,
467 Svc::Error: Into<BoxError>,
468 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
469 B::Error: Into<BoxError>,
470{
471 pub fn with_executor(
472 self,
473 executor: Executor,
474 ) -> OctocrabBuilder<Svc, Config, Auth, LayerReady> {
475 OctocrabBuilder {
476 service: self.service,
477 auth: self.auth,
478 config: self.config,
479 _layer_ready: PhantomData,
480 executor: Some(executor),
481 }
482 }
483}
484
485impl<Svc, Config, Auth, B> OctocrabBuilder<Svc, Config, Auth, LayerReady>
486where
487 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
488 Svc::Future: Send + 'static,
489 Svc::Error: Into<BoxError>,
490 B: http_body::Body<Data = bytes::Bytes> + Send + 'static,
491 B::Error: Into<BoxError>,
492{
493 pub fn with_layer<L: Layer<Svc>>(
495 self,
496 layer: &L,
497 ) -> OctocrabBuilder<L::Service, Config, Auth, LayerReady> {
498 let Self {
499 service: stack,
500 auth,
501 config,
502 executor,
503 ..
504 } = self;
505 OctocrabBuilder {
506 service: layer.layer(stack),
507 auth,
508 config,
509 executor,
510 _layer_ready: PhantomData,
511 }
512 }
513}
514
515impl Default for OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
516 fn default() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
517 OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
518 }
519}
520
521impl<Svc, Auth, LayerState> OctocrabBuilder<Svc, NoConfig, Auth, LayerState> {
522 fn with_config<Config>(self, config: Config) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
523 OctocrabBuilder {
524 service: self.service,
525 auth: self.auth,
526 executor: self.executor,
527 config,
528 _layer_ready: PhantomData,
529 }
530 }
531}
532
533impl<Svc, B, LayerState> OctocrabBuilder<Svc, NoConfig, AuthState, LayerState>
534where
535 Svc: Service<Request<OctoBody>, Response = Response<B>> + Send + 'static,
536 Svc::Future: Send + 'static,
537 Svc::Error: Into<BoxError>,
538 B: http_body::Body<Data = bytes::Bytes> + Send + Sync + 'static,
539 B::Error: Into<BoxError>,
540{
541 pub fn build(self) -> Result<Octocrab, Infallible> {
543 let service = MapResponseBodyLayer::new(|b: B| {
545 b.map_err(|e| ServiceSnafu.into_error(e.into())).boxed()
546 })
547 .layer(self.service)
548 .map_err(|e| e.into());
549
550 if let Some(executor) = self.executor {
551 return Ok(Octocrab::new_with_executor(service, self.auth, executor));
552 }
553
554 Ok(Octocrab::new(service, self.auth))
555 }
556}
557
558impl<Svc, Config, LayerState> OctocrabBuilder<Svc, Config, NoAuth, LayerState> {
559 pub fn with_auth<Auth>(self, auth: Auth) -> OctocrabBuilder<Svc, Config, Auth, LayerState> {
560 OctocrabBuilder {
561 service: self.service,
562 auth,
563 config: self.config,
564 executor: self.executor,
565 _layer_ready: PhantomData,
566 }
567 }
568}
569
570impl OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady> {
571 #[cfg(feature = "retry")]
573 #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
574 pub fn add_retry_config(mut self, retry_config: RetryConfig) -> Self {
575 self.config.retry_config = retry_config;
576 self
577 }
578
579 #[cfg(feature = "timeout")]
581 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
582 pub fn set_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
583 self.config.connect_timeout = timeout;
584 self
585 }
586
587 #[cfg(feature = "timeout")]
589 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
590 pub fn set_read_timeout(mut self, timeout: Option<Duration>) -> Self {
591 self.config.read_timeout = timeout;
592 self
593 }
594
595 #[cfg(feature = "timeout")]
597 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
598 pub fn set_write_timeout(mut self, timeout: Option<Duration>) -> Self {
599 self.config.write_timeout = timeout;
600 self
601 }
602
603 pub fn add_preview(mut self, preview: &'static str) -> Self {
605 self.config.previews.push(preview);
606 self
607 }
608
609 pub fn add_header(mut self, key: HeaderName, value: String) -> Self {
611 self.config.extra_headers.push((key, value));
612 self
613 }
614
615 pub fn personal_token<S: Into<SecretString>>(mut self, token: S) -> Self {
617 self.config.auth = Auth::PersonalToken(token.into());
618 self
619 }
620
621 pub fn app(mut self, app_id: AppId, key: jsonwebtoken::EncodingKey) -> Self {
624 self.config.auth = Auth::App(AppAuth { app_id, key });
625 self
626 }
627
628 pub fn basic_auth(mut self, username: String, password: String) -> Self {
631 self.config.auth = Auth::Basic { username, password };
632 self
633 }
634
635 pub fn oauth(mut self, oauth: auth::OAuth) -> Self {
637 self.config.auth = Auth::OAuth(oauth);
638 self
639 }
640
641 pub fn user_access_token<S: Into<SecretString>>(mut self, token: S) -> Self {
643 self.config.auth = Auth::UserAccessToken(token.into());
644 self
645 }
646
647 pub fn base_uri(mut self, base_uri: impl TryInto<Uri>) -> Result<Self> {
649 self.config.base_uri = Some(
650 base_uri
651 .try_into()
652 .map_err(|_| UriParseError {})
653 .context(UriParseSnafu)?,
654 );
655 Ok(self)
656 }
657
658 pub fn upload_uri(mut self, upload_uri: impl TryInto<Uri>) -> Result<Self> {
660 self.config.upload_uri = Some(
661 upload_uri
662 .try_into()
663 .map_err(|_| UriParseError {})
664 .context(UriParseSnafu)?,
665 );
666 Ok(self)
667 }
668
669 #[cfg(feature = "retry")]
670 #[cfg_attr(docsrs, doc(cfg(feature = "retry")))]
671 pub fn set_connector_retry_service<S>(
672 &self,
673 connector: hyper_util::client::legacy::Client<S, OctoBody>,
674 ) -> Retry<RetryConfig, hyper_util::client::legacy::Client<S, OctoBody>> {
675 let retry_layer = RetryLayer::new(self.config.retry_config.clone());
676
677 retry_layer.layer(connector)
678 }
679
680 #[cfg(feature = "timeout")]
681 #[cfg_attr(docsrs, doc(cfg(feature = "timeout")))]
682 pub fn set_connect_timeout_service<T>(&self, connector: T) -> TimeoutConnector<T>
683 where
684 T: Service<Uri> + Send,
685 T::Response: hyper::rt::Read + hyper::rt::Write + Send + Unpin,
686 T::Future: Send + 'static,
687 T::Error: Into<BoxError>,
688 {
689 let mut connector = TimeoutConnector::new(connector);
690 connector.set_connect_timeout(self.config.connect_timeout);
692 connector.set_read_timeout(self.config.read_timeout);
693 connector.set_write_timeout(self.config.write_timeout);
694 connector
695 }
696
697 #[cfg(feature = "default-client")]
699 #[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
700 pub fn build(self) -> Result<Octocrab> {
701 let client: hyper_util::client::legacy::Client<_, OctoBody> = {
702 #[cfg(all(not(feature = "opentls"), not(feature = "rustls")))]
703 let mut connector = hyper::client::conn::http1::HttpConnector::new();
704
705 #[cfg(all(feature = "rustls", not(feature = "opentls")))]
706 let connector = {
707 let builder = HttpsConnectorBuilder::new();
708 #[cfg(feature = "rustls-webpki-tokio")]
709 let builder = builder.with_webpki_roots();
710 #[cfg(not(feature = "rustls-webpki-tokio"))]
711 let builder = builder
712 .with_native_roots()
713 .map_err(Into::into)
714 .context(error::OtherSnafu)?; builder
717 .https_or_http() .enable_http1()
719 .build()
720 };
721
722 #[cfg(all(feature = "opentls", not(feature = "rustls")))]
723 let connector = HttpsConnector::new();
724
725 #[cfg(feature = "timeout")]
726 let connector = self.set_connect_timeout_service(connector);
727
728 hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
729 .build(connector)
730 };
731
732 #[cfg(feature = "retry")]
733 let client = self.set_connector_retry_service(client);
734
735 #[cfg(feature = "tracing")]
736 let client = TraceLayer::new_for_http()
737 .make_span_with(|req: &Request<OctoBody>| {
738 tracing::debug_span!(
739 "HTTP",
740 http.method = %req.method(),
741 http.url = %req.uri(),
742 http.status_code = tracing::field::Empty,
743 otel.name = req.extensions().get::<&'static str>().unwrap_or(&"HTTP"),
744 otel.kind = "client",
745 otel.status_code = tracing::field::Empty,
746 )
747 })
748 .on_request(|_req: &Request<OctoBody>, _span: &Span| {
749 tracing::debug!("requesting");
750 })
751 .on_response(
752 |res: &Response<hyper::body::Incoming>, _latency: Duration, span: &Span| {
753 let status = res.status();
754 span.record("http.status_code", status.as_u16());
755 if status.is_client_error() || status.is_server_error() {
756 span.record("otel.status_code", "ERROR");
757 }
758 },
759 )
760 .on_body_chunk(())
762 .on_eos(|_: Option<&HeaderMap>, _duration: Duration, _span: &Span| {
763 tracing::debug!("stream closed");
764 })
765 .on_failure(
766 |ec: ServerErrorsFailureClass, _latency: Duration, span: &Span| {
767 span.record("otel.status_code", "ERROR");
773 match ec {
774 ServerErrorsFailureClass::StatusCode(status) => {
775 span.record("http.status_code", status.as_u16());
776 tracing::error!("failed with status {}", status)
777 }
778 ServerErrorsFailureClass::Error(err) => {
779 tracing::error!("failed with error {}", err)
780 }
781 }
782 },
783 )
784 .layer(client);
785
786 #[cfg(feature = "follow-redirect")]
787 let client = tower_http::follow_redirect::FollowRedirectLayer::new().layer(client);
788
789 let mut hmap: Vec<(HeaderName, HeaderValue)> = vec![];
790
791 hmap.push((USER_AGENT, HeaderValue::from_str("octocrab").unwrap()));
793
794 for preview in &self.config.previews {
795 hmap.push((
796 http::header::ACCEPT,
797 HeaderValue::from_str(crate::format_preview(preview).as_str()).unwrap(),
798 ));
799 }
800
801 let (auth_header, auth_state): (Option<HeaderValue>, _) = match self.config.auth {
802 Auth::None => (None, AuthState::None),
803 Auth::Basic { username, password } => {
804 (None, AuthState::BasicAuth { username, password })
805 }
806 Auth::PersonalToken(token) => (
807 Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
808 AuthState::None,
809 ),
810 Auth::UserAccessToken(token) => (
811 Some(format!("Bearer {}", token.expose_secret()).parse().unwrap()),
812 AuthState::None,
813 ),
814 Auth::App(app_auth) => (None, AuthState::App(app_auth)),
815 Auth::OAuth(device) => (
816 Some(
817 format!(
818 "{} {}",
819 device.token_type,
820 &device.access_token.expose_secret()
821 )
822 .parse()
823 .unwrap(),
824 ),
825 AuthState::None,
826 ),
827 };
828
829 for (key, value) in self.config.extra_headers.iter() {
830 hmap.push((
831 key.clone(),
832 HeaderValue::from_str(value.as_str())
833 .map_err(http::Error::from)
834 .context(HttpSnafu)?,
835 ));
836 }
837
838 let client = ExtraHeadersLayer::new(Arc::new(hmap)).layer(client);
839
840 let client = MapResponseBodyLayer::new(|body| {
841 BodyExt::map_err(body, |e| HyperSnafu.into_error(e)).boxed()
842 })
843 .layer(client);
844
845 let base_uri = self
846 .config
847 .base_uri
848 .clone()
849 .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_URI).unwrap());
850
851 let upload_uri = self
852 .config
853 .upload_uri
854 .clone()
855 .unwrap_or_else(|| Uri::from_str(GITHUB_BASE_UPLOAD_URI).unwrap());
856
857 let client = BaseUriLayer::new(base_uri.clone()).layer(client);
858
859 let client = AuthHeaderLayer::new(auth_header, base_uri, upload_uri).layer(client);
860
861 if let Some(executor) = self.executor {
862 return Ok(Octocrab::new_with_executor(client, auth_state, executor));
863 }
864
865 Ok(Octocrab::new(client, auth_state))
866 }
867}
868
869pub struct DefaultOctocrabBuilderConfig {
870 auth: Auth,
871 previews: Vec<&'static str>,
872 extra_headers: Vec<(HeaderName, String)>,
873 #[cfg(feature = "timeout")]
874 connect_timeout: Option<Duration>,
875 #[cfg(feature = "timeout")]
876 read_timeout: Option<Duration>,
877 #[cfg(feature = "timeout")]
878 write_timeout: Option<Duration>,
879 base_uri: Option<Uri>,
880 upload_uri: Option<Uri>,
881 #[cfg(feature = "retry")]
882 retry_config: RetryConfig,
883}
884
885impl Default for DefaultOctocrabBuilderConfig {
886 fn default() -> Self {
887 Self {
888 auth: Auth::None,
889 previews: Vec::new(),
890 extra_headers: Vec::new(),
891 #[cfg(feature = "timeout")]
892 connect_timeout: None,
893 #[cfg(feature = "timeout")]
894 read_timeout: None,
895 #[cfg(feature = "timeout")]
896 write_timeout: None,
897 base_uri: None,
898 upload_uri: None,
899 #[cfg(feature = "retry")]
900 retry_config: RetryConfig::Simple(3),
901 }
902 }
903}
904
905impl DefaultOctocrabBuilderConfig {
906 pub fn new() -> Self {
907 Self::default()
908 }
909}
910
911#[derive(Debug, Clone)]
912struct CachedTokenInner {
913 expiration: Option<DateTime<Utc>>,
914 secret: SecretString,
915}
916
917impl CachedTokenInner {
918 fn new(secret: SecretString, expiration: Option<DateTime<Utc>>) -> Self {
919 Self { secret, expiration }
920 }
921
922 fn expose_secret(&self) -> &str {
923 self.secret.expose_secret()
924 }
925}
926
927pub struct CachedToken(RwLock<Option<CachedTokenInner>>);
929
930impl CachedToken {
931 fn clear(&self) {
932 *self.0.write().unwrap() = None;
933 }
934
935 fn valid_token_with_buffer(&self, buffer: chrono::Duration) -> Option<SecretString> {
937 let inner = self.0.read().unwrap();
938
939 if let Some(token) = inner.as_ref() {
940 if let Some(exp) = token.expiration {
941 if exp - Utc::now() > buffer {
942 return Some(token.secret.clone());
943 }
944 } else {
945 return Some(token.secret.clone());
946 }
947 }
948
949 None
950 }
951
952 fn valid_token(&self) -> Option<SecretString> {
953 self.valid_token_with_buffer(chrono::Duration::seconds(30))
954 }
955
956 fn set<S: Into<SecretString>>(&self, token: S, expiration: Option<DateTime<Utc>>) {
957 *self.0.write().unwrap() = Some(CachedTokenInner::new(token.into(), expiration));
958 }
959}
960
961impl fmt::Debug for CachedToken {
962 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
963 self.0.read().unwrap().fmt(f)
964 }
965}
966
967impl fmt::Display for CachedToken {
968 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
969 let option = self.0.read().unwrap();
970 option
971 .as_ref()
972 .map(|s| s.expose_secret().fmt(f))
973 .unwrap_or_else(|| write!(f, "<none>"))
974 }
975}
976
977impl Clone for CachedToken {
978 fn clone(&self) -> CachedToken {
979 CachedToken(RwLock::new(self.0.read().unwrap().clone()))
980 }
981}
982
983impl Default for CachedToken {
984 fn default() -> CachedToken {
985 CachedToken(RwLock::new(None))
986 }
987}
988
989#[derive(Debug, Clone)]
991pub enum AuthState {
992 None,
995 BasicAuth {
997 username: String,
999 password: String,
1001 },
1002 App(AppAuth),
1004 Installation {
1006 app: AppAuth,
1008 installation: InstallationId,
1010 token: CachedToken,
1012 },
1013 AccessToken {
1015 token: SecretString,
1017 },
1018}
1019
1020pub type OctocrabService = Buffer<
1021 http::Request<OctoBody>,
1022 <BoxService<http::Request<OctoBody>, http::Response<BoxBody<Bytes, Error>>, BoxError> as tower::Service<http::Request<OctoBody>>>::Future
1023>;
1024
1025#[derive(Clone)]
1027pub struct Octocrab {
1028 client: OctocrabService,
1029 auth_state: AuthState,
1030}
1031
1032impl fmt::Debug for Octocrab {
1033 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034 f.debug_struct("Octocrab")
1035 .field("auth_state", &self.auth_state)
1036 .finish()
1037 }
1038}
1039
1040#[cfg(feature = "default-client")]
1045#[cfg_attr(docsrs, doc(cfg(feature = "default-client")))]
1046impl Default for Octocrab {
1047 fn default() -> Self {
1048 OctocrabBuilder::default().build().unwrap()
1049 }
1050}
1051
1052impl Octocrab {
1054 pub fn builder() -> OctocrabBuilder<NoSvc, DefaultOctocrabBuilderConfig, NoAuth, NotLayerReady>
1056 {
1057 OctocrabBuilder::new_empty().with_config(DefaultOctocrabBuilderConfig::default())
1058 }
1059
1060 fn new<S>(service: S, auth_state: AuthState) -> Self
1062 where
1063 S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1064 + Send
1065 + 'static,
1066 S::Future: Send + 'static,
1067 S::Error: Into<BoxError>,
1068 {
1069 let service = Buffer::new(BoxService::new(service.map_err(Into::into)), 1024);
1070
1071 Self {
1072 client: service,
1073 auth_state,
1074 }
1075 }
1076
1077 fn new_with_executor<S>(service: S, auth_state: AuthState, executor: Executor) -> Self
1079 where
1080 S: Service<Request<OctoBody>, Response = Response<BoxBody<Bytes, crate::Error>>>
1081 + Send
1082 + 'static,
1083 S::Future: Send + 'static,
1084 S::Error: Into<BoxError>,
1085 {
1086 let (service, worker) = Buffer::pair(BoxService::new(service.map_err(Into::into)), 1024);
1088
1089 executor(Box::pin(worker));
1091
1092 Self {
1093 client: service,
1094 auth_state,
1095 }
1096 }
1097
1098 pub fn installation(&self, id: InstallationId) -> Result<Octocrab> {
1106 let app_auth = if let AuthState::App(ref app_auth) = self.auth_state {
1107 app_auth.clone()
1108 } else {
1109 return Err(Error::Installation {
1110 backtrace: Backtrace::capture(),
1111 });
1112 };
1113 Ok(Octocrab {
1114 client: self.client.clone(),
1115 auth_state: AuthState::Installation {
1116 app: app_auth,
1117 installation: id,
1118 token: CachedToken::default(),
1119 },
1120 })
1121 }
1122
1123 pub async fn installation_and_token(
1130 &self,
1131 id: InstallationId,
1132 ) -> Result<(Octocrab, SecretString)> {
1133 let crab = self.installation(id)?;
1134 let token = crab.request_installation_auth_token().await?;
1135 Ok((crab, token))
1136 }
1137
1138 pub fn user_access_token<S: Into<SecretString>>(&self, token: S) -> Result<Self> {
1143 Ok(Octocrab {
1144 client: self.client.clone(),
1145 auth_state: AuthState::AccessToken {
1146 token: token.into(),
1147 },
1148 })
1149 }
1150}
1151
1152impl Octocrab {
1154 pub fn actions(&self) -> actions::ActionsHandler<'_> {
1157 actions::ActionsHandler::new(self)
1158 }
1159
1160 pub fn current(&self) -> current::CurrentAuthHandler<'_> {
1163 current::CurrentAuthHandler::new(self)
1164 }
1165
1166 pub fn activity(&self) -> activity::ActivityHandler<'_> {
1168 activity::ActivityHandler::new(self)
1169 }
1170
1171 pub fn apps(&self) -> apps::AppsRequestHandler<'_> {
1173 apps::AppsRequestHandler::new(self)
1174 }
1175
1176 pub fn gitignore(&self) -> gitignore::GitignoreHandler<'_> {
1179 gitignore::GitignoreHandler::new(self)
1180 }
1181
1182 pub fn issues(
1185 &self,
1186 owner: impl Into<String>,
1187 repo: impl Into<String>,
1188 ) -> issues::IssueHandler<'_> {
1189 issues::IssueHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1190 }
1191
1192 pub fn issues_by_id(&self, id: impl Into<RepositoryId>) -> issues::IssueHandler<'_> {
1195 issues::IssueHandler::new(self, RepoRef::ById(id.into()))
1196 }
1197
1198 pub fn code_scannings(
1201 &self,
1202 owner: impl Into<String>,
1203 repo: impl Into<String>,
1204 ) -> code_scannings::CodeScanningHandler<'_> {
1205 code_scannings::CodeScanningHandler::new(self, owner.into(), Option::from(repo.into()))
1206 }
1207
1208 pub fn code_scannings_organisation(
1211 &self,
1212 owner: impl Into<String>,
1213 ) -> code_scannings::CodeScanningHandler<'_> {
1214 code_scannings::CodeScanningHandler::new(self, owner.into(), None)
1215 }
1216
1217 pub fn commits(
1219 &self,
1220 owner: impl Into<String>,
1221 repo: impl Into<String>,
1222 ) -> commits::CommitHandler<'_> {
1223 commits::CommitHandler::new(self, owner.into(), repo.into())
1224 }
1225
1226 pub fn licenses(&self) -> licenses::LicenseHandler<'_> {
1228 licenses::LicenseHandler::new(self)
1229 }
1230
1231 pub fn markdown(&self) -> markdown::MarkdownHandler<'_> {
1233 markdown::MarkdownHandler::new(self)
1234 }
1235
1236 pub fn orgs(&self, owner: impl Into<String>) -> orgs::OrgHandler<'_> {
1239 orgs::OrgHandler::new(self, owner.into())
1240 }
1241
1242 pub fn pulls(
1245 &self,
1246 owner: impl Into<String>,
1247 repo: impl Into<String>,
1248 ) -> pulls::PullRequestHandler<'_> {
1249 pulls::PullRequestHandler::new(self, owner.into(), repo.into())
1250 }
1251
1252 pub fn repos(
1255 &self,
1256 owner: impl Into<String>,
1257 repo: impl Into<String>,
1258 ) -> repos::RepoHandler<'_> {
1259 repos::RepoHandler::new(self, RepoRef::ByOwnerAndName(owner.into(), repo.into()))
1260 }
1261
1262 pub fn repos_by_id(&self, id: impl Into<RepositoryId>) -> repos::RepoHandler<'_> {
1265 repos::RepoHandler::new(self, RepoRef::ById(id.into()))
1266 }
1267
1268 pub fn projects(&self) -> projects::ProjectHandler<'_> {
1271 projects::ProjectHandler::new(self)
1272 }
1273
1274 pub fn search(&self) -> search::SearchHandler<'_> {
1277 search::SearchHandler::new(self)
1278 }
1279
1280 pub fn teams(&self, owner: impl Into<String>) -> teams::TeamHandler<'_> {
1283 teams::TeamHandler::new(self, owner.into())
1284 }
1285
1286 pub fn users(&self, user: impl Into<String>) -> users::UserHandler<'_> {
1288 users::UserHandler::new(self, UserRef::ByString(user.into()))
1289 }
1290
1291 pub fn users_by_id(&self, user: impl Into<UserId>) -> users::UserHandler<'_> {
1293 users::UserHandler::new(self, UserRef::ById(user.into()))
1294 }
1295
1296 pub fn workflows(
1299 &self,
1300 owner: impl Into<String>,
1301 repo: impl Into<String>,
1302 ) -> workflows::WorkflowsHandler<'_> {
1303 workflows::WorkflowsHandler::new(self, owner.into(), repo.into())
1304 }
1305
1306 pub fn events(&self) -> events::EventsBuilder<'_> {
1309 events::EventsBuilder::new(self)
1310 }
1311
1312 pub fn gists(&self) -> gists::GistsHandler<'_> {
1315 gists::GistsHandler::new(self)
1316 }
1317
1318 pub fn checks(
1320 &self,
1321 owner: impl Into<String>,
1322 repo: impl Into<String>,
1323 ) -> checks::ChecksHandler<'_> {
1324 checks::ChecksHandler::new(self, owner.into(), repo.into())
1325 }
1326
1327 pub fn ratelimit(&self) -> ratelimit::RateLimitHandler<'_> {
1329 ratelimit::RateLimitHandler::new(self)
1330 }
1331
1332 pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler<'_> {
1334 hooks::HooksHandler::new(self, owner.into())
1335 }
1336}
1337
1338impl Octocrab {
1340 pub async fn graphql<R: crate::FromResponse>(
1351 &self,
1352 payload: &(impl serde::Serialize + ?Sized),
1353 ) -> crate::Result<R> {
1354 self.post("/graphql", Some(&serde_json::json!(payload)))
1355 .await
1356 }
1357}
1358
1359impl Octocrab {
1371 pub async fn post<P: Serialize + ?Sized, R: FromResponse>(
1374 &self,
1375 route: impl AsRef<str>,
1376 body: Option<&P>,
1377 ) -> Result<R> {
1378 let response = self
1379 ._post(self.parameterized_uri(route, None::<&()>)?, body)
1380 .await?;
1381 R::from_response(crate::map_github_error(response).await?).await
1382 }
1383
1384 pub async fn _post<P: Serialize + ?Sized>(
1386 &self,
1387 uri: impl TryInto<http::Uri>,
1388 body: Option<&P>,
1389 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1390 let uri = uri
1391 .try_into()
1392 .map_err(|_| UriParseError {})
1393 .context(UriParseSnafu)?;
1394 let request = Builder::new().method(Method::POST).uri(uri);
1395 let request = self.build_request(request, body)?;
1396 self.execute(request).await
1397 }
1398
1399 pub async fn get<R, A, P>(&self, route: A, parameters: Option<&P>) -> Result<R>
1402 where
1403 A: AsRef<str>,
1404 P: Serialize + ?Sized,
1405 R: FromResponse,
1406 {
1407 self.get_with_headers(route, parameters, None).await
1408 }
1409
1410 pub async fn _get(
1412 &self,
1413 uri: impl TryInto<Uri>,
1414 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1415 self._get_with_headers(uri, None).await
1416 }
1417
1418 fn parameterized_uri<A, P>(&self, uri: A, parameters: Option<&P>) -> Result<Uri>
1421 where
1422 A: AsRef<str>,
1423 P: Serialize + ?Sized,
1424 {
1425 let mut uri = uri.as_ref().to_string();
1426 if let Some(parameters) = parameters {
1427 if uri.contains('?') {
1428 uri = format!("{uri}&");
1429 } else {
1430 uri = format!("{uri}?");
1431 }
1432 uri = format!(
1433 "{}{}",
1434 uri,
1435 serde_urlencoded::to_string(parameters)
1436 .context(SerdeUrlEncodedSnafu)?
1437 .as_str()
1438 );
1439 }
1440 let uri = Uri::from_str(uri.as_str()).context(UriSnafu);
1441 uri
1442 }
1443
1444 pub async fn body_to_string(
1445 &self,
1446 res: http::Response<BoxBody<Bytes, crate::Error>>,
1447 ) -> Result<String> {
1448 let body_bytes = res.into_body().collect().await?.to_bytes();
1449 String::from_utf8(body_bytes.to_vec()).context(InvalidUtf8Snafu)
1450 }
1451
1452 pub async fn get_with_headers<R, A, P>(
1455 &self,
1456 route: A,
1457 parameters: Option<&P>,
1458 headers: Option<http::header::HeaderMap>,
1459 ) -> Result<R>
1460 where
1461 A: AsRef<str>,
1462 P: Serialize + ?Sized,
1463 R: FromResponse,
1464 {
1465 let response = self
1466 ._get_with_headers(self.parameterized_uri(route, parameters)?, headers)
1467 .await?;
1468 R::from_response(crate::map_github_error(response).await?).await
1469 }
1470
1471 pub async fn _get_with_headers(
1473 &self,
1474 uri: impl TryInto<Uri>,
1475 headers: Option<http::header::HeaderMap>,
1476 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1477 let uri = uri
1478 .try_into()
1479 .map_err(|_| UriParseError {})
1480 .context(UriParseSnafu)?;
1481 let mut request = Builder::new().method(Method::GET).uri(uri);
1482 if let Some(headers) = headers {
1483 for (key, value) in headers.iter() {
1484 request = request.header(key, value);
1485 }
1486 }
1487 let request = self.build_request(request, None::<&()>)?;
1488 self.execute(request).await
1489 }
1490
1491 pub async fn patch<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1494 where
1495 A: AsRef<str>,
1496 B: Serialize + ?Sized,
1497 R: FromResponse,
1498 {
1499 let response = self
1500 ._patch(self.parameterized_uri(route, None::<&()>)?, body)
1501 .await?;
1502 R::from_response(crate::map_github_error(response).await?).await
1503 }
1504
1505 pub async fn _patch<B: Serialize + ?Sized>(
1507 &self,
1508 uri: impl TryInto<Uri>,
1509 body: Option<&B>,
1510 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1511 let uri = uri
1512 .try_into()
1513 .map_err(|_| UriParseError {})
1514 .context(UriParseSnafu)?;
1515 let request = Builder::new().method(Method::PATCH).uri(uri);
1516 let request = self.build_request(request, body)?;
1517 self.execute(request).await
1518 }
1519
1520 pub async fn put<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1523 where
1524 A: AsRef<str>,
1525 B: Serialize + ?Sized,
1526 R: FromResponse,
1527 {
1528 let response = self
1529 ._put(self.parameterized_uri(route, None::<&()>)?, body)
1530 .await?;
1531 R::from_response(crate::map_github_error(response).await?).await
1532 }
1533
1534 pub async fn _put<B: Serialize + ?Sized>(
1536 &self,
1537 uri: impl TryInto<Uri>,
1538 body: Option<&B>,
1539 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1540 let uri = uri
1541 .try_into()
1542 .map_err(|_| UriParseError {})
1543 .context(UriParseSnafu)?;
1544 let request = Builder::new().method(Method::PUT).uri(uri);
1545 let request = self.build_request(request, body)?;
1546 self.execute(request).await
1547 }
1548
1549 pub fn build_request<B: Serialize + ?Sized>(
1550 &self,
1551 mut builder: Builder,
1552 body: Option<&B>,
1553 ) -> Result<http::Request<OctoBody>> {
1554 if let Some(body) = body {
1561 builder = builder.header(http::header::CONTENT_TYPE, "application/json");
1562 let serialized = serde_json::to_string(body).context(SerdeSnafu)?;
1563 let body: OctoBody = serialized.into();
1564 let request = builder.body(body).context(HttpSnafu)?;
1565 Ok(request)
1566 } else {
1567 Ok(builder
1568 .header(http::header::CONTENT_LENGTH, "0")
1569 .body(OctoBody::empty())
1570 .context(HttpSnafu)?)
1571 }
1572 }
1573
1574 pub async fn delete<R, A, B>(&self, route: A, body: Option<&B>) -> Result<R>
1577 where
1578 A: AsRef<str>,
1579 B: Serialize + ?Sized,
1580 R: FromResponse,
1581 {
1582 let response = self
1583 ._delete(self.parameterized_uri(route, None::<&()>)?, body)
1584 .await?;
1585 R::from_response(crate::map_github_error(response).await?).await
1586 }
1587
1588 pub async fn _delete<B: Serialize + ?Sized>(
1590 &self,
1591 uri: impl TryInto<Uri>,
1592 body: Option<&B>,
1593 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1594 let uri = uri
1595 .try_into()
1596 .map_err(|_| UriParseError {})
1597 .context(UriParseSnafu)?;
1598 let request = self.build_request(Builder::new().method(Method::DELETE).uri(uri), body)?;
1599
1600 self.execute(request).await
1601 }
1602
1603 async fn request_installation_auth_token(&self) -> Result<SecretString> {
1605 let (app, installation, token) = if let AuthState::Installation {
1606 ref app,
1607 installation,
1608 ref token,
1609 } = self.auth_state
1610 {
1611 (app, installation, token)
1612 } else {
1613 return Err(Error::Installation {
1614 backtrace: Backtrace::capture(),
1615 });
1616 };
1617 let mut request = Builder::new();
1618 let mut sensitive_value =
1619 HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1620 .map_err(http::Error::from)
1621 .context(HttpSnafu)?;
1622
1623 let uri = http::Uri::builder()
1624 .path_and_query(format!("/app/installations/{installation}/access_tokens"))
1625 .build()
1626 .context(HttpSnafu)?;
1627
1628 sensitive_value.set_sensitive(true);
1629 request = request
1630 .header(http::header::AUTHORIZATION, sensitive_value)
1631 .method(http::Method::POST)
1632 .uri(uri);
1633 let response = self
1634 .send(request.body("{}".into()).context(HttpSnafu)?)
1635 .await?;
1636 let _status = response.status();
1637
1638 let token_object =
1639 InstallationToken::from_response(crate::map_github_error(response).await?).await?;
1640
1641 let expiration = token_object
1642 .expires_at
1643 .map(|time| {
1644 DateTime::<Utc>::from_str(&time).map_err(|e| error::Error::Other {
1645 source: Box::new(e),
1646 backtrace: snafu::Backtrace::capture(),
1647 })
1648 })
1649 .transpose()?;
1650
1651 #[cfg(feature = "tracing")]
1652 tracing::debug!("Token expires at: {:?}", expiration);
1653
1654 token.set(token_object.token.clone(), expiration);
1655
1656 Ok(SecretString::from(token_object.token))
1657 }
1658
1659 pub async fn send(
1661 &self,
1662 request: Request<OctoBody>,
1663 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1664 let mut svc = self.client.clone();
1665 let response: Response<BoxBody<Bytes, crate::Error>> = svc
1666 .ready()
1667 .await
1668 .context(ServiceSnafu)?
1669 .call(request)
1670 .await
1671 .context(ServiceSnafu)?;
1672 Ok(response)
1673 }
1684
1685 pub async fn execute(
1687 &self,
1688 request: http::Request<impl Into<OctoBody>>,
1689 ) -> Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1690 let (mut parts, body) = request.into_parts();
1691 let body: OctoBody = body.into();
1692 let auth_header: Option<HeaderValue> = match self.auth_state {
1694 AuthState::None => None,
1695 AuthState::App(ref app) => Some(
1696 HeaderValue::from_str(format!("Bearer {}", app.generate_bearer_token()?).as_str())
1697 .map_err(http::Error::from)
1698 .context(HttpSnafu)?,
1699 ),
1700 AuthState::BasicAuth {
1701 ref username,
1702 ref password,
1703 } => {
1704 use base64::prelude::BASE64_STANDARD;
1706 use base64::write::EncoderWriter;
1707
1708 let mut buf = b"Basic ".to_vec();
1709 {
1710 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
1711 write!(encoder, "{username}:{password}").expect("writing to a Vec never fails");
1712 }
1713 Some(HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue"))
1714 }
1715 AuthState::Installation { ref token, .. } => {
1716 let token = if let Some(token) = token.valid_token() {
1717 token
1718 } else {
1719 self.request_installation_auth_token().await?
1720 };
1721
1722 Some(
1723 HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1724 .map_err(http::Error::from)
1725 .context(HttpSnafu)?,
1726 )
1727 }
1728 AuthState::AccessToken { ref token } => Some(
1729 HeaderValue::from_str(format!("Bearer {}", token.expose_secret()).as_str())
1730 .map_err(http::Error::from)
1731 .context(HttpSnafu)?,
1732 ),
1733 };
1734
1735 if let Some(mut auth_header) = auth_header {
1736 match parts.uri.authority() {
1741 None => {
1742 auth_header.set_sensitive(true);
1743 parts
1744 .headers
1745 .insert(http::header::AUTHORIZATION, auth_header);
1746 }
1747 Some(authority) if authority == "api.github.com" => {
1748 auth_header.set_sensitive(true);
1749 parts
1750 .headers
1751 .insert(http::header::AUTHORIZATION, auth_header);
1752 }
1753 Some(_) => {
1754 }
1756 }
1757 }
1758
1759 let request = http::Request::from_parts(parts, body);
1760
1761 let response = self.send(request).await?;
1762
1763 let status = response.status();
1764 if StatusCode::UNAUTHORIZED == status {
1765 if let AuthState::Installation { ref token, .. } = self.auth_state {
1766 token.clear();
1767 }
1768 }
1769 Ok(response)
1770 }
1771
1772 pub async fn follow_location_to_data(
1773 &self,
1774 response: http::Response<BoxBody<Bytes, Error>>,
1775 ) -> crate::Result<http::Response<BoxBody<Bytes, crate::Error>>> {
1776 if let Some(redirect) = response.headers().get(http::header::LOCATION) {
1777 let location = redirect.to_str().expect("Location URL not valid str");
1778
1779 self._get(location).await
1780 } else {
1781 Ok(response)
1782 }
1783 }
1784
1785 pub async fn download(
1790 &self,
1791 uri: impl TryInto<Uri>,
1792 content_type: impl TryInto<http::HeaderValue>,
1793 ) -> crate::Result<Vec<u8>> {
1794 let uri = uri
1795 .try_into()
1796 .map_err(|_| UriParseError {})
1797 .context(UriParseSnafu)?;
1798 let content_type = content_type
1799 .try_into()
1800 .map_err(|_| UriParseError {})
1801 .context(UriParseSnafu)?;
1802
1803 let mut request = Builder::new().method(Method::GET).uri(uri);
1804 request = request.header(http::header::ACCEPT, content_type);
1805
1806 let request = self.build_request(request, None::<&()>)?;
1807 let response = self.execute(request).await?;
1808
1809 let bytes = response.into_body().collect().await?.to_bytes();
1810 Ok(bytes.to_vec())
1811 }
1812
1813 pub async fn download_zip(&self, uri: impl TryInto<Uri>) -> crate::Result<Vec<u8>> {
1815 self.download(uri, "application/zip").await
1816 }
1817}
1818
1819impl Octocrab {
1821 pub async fn get_page<R: serde::de::DeserializeOwned>(
1823 &self,
1824 uri: &Option<Uri>,
1825 ) -> crate::Result<Option<Page<R>>> {
1826 match uri {
1827 Some(uri) => self.get(uri.to_string(), None::<&()>).await.map(Some),
1828 None => Ok(None),
1829 }
1830 }
1831
1832 pub async fn all_pages<R: serde::de::DeserializeOwned>(
1835 &self,
1836 mut page: Page<R>,
1837 ) -> crate::Result<Vec<R>> {
1838 let mut ret = page.take_items();
1839 while let Some(mut next_page) = self.get_page(&page.next).await? {
1840 ret.append(&mut next_page.take_items());
1841 page = next_page;
1842 }
1843 Ok(ret)
1844 }
1845}
1846
1847#[cfg(test)]
1848mod tests {
1849 #[tokio::test]
1851 async fn parametrize_uri_valid() {
1852 let uri = crate::instance()
1855 .parameterized_uri("/help%20world", None::<&()>)
1856 .unwrap();
1857 assert_eq!(uri.path(), "/help%20world");
1858 }
1859
1860 #[tokio::test]
1861 async fn extra_headers() {
1862 use http::header::HeaderName;
1863 use wiremock::{matchers, Mock, MockServer, ResponseTemplate};
1864 let response = ResponseTemplate::new(304).append_header("etag", "\"abcd\"");
1865 let mock_server = MockServer::start().await;
1866 Mock::given(matchers::method("GET"))
1867 .and(matchers::path_regex(".*"))
1868 .and(matchers::header("x-test1", "hello"))
1869 .and(matchers::header("x-test2", "goodbye"))
1870 .respond_with(response)
1871 .expect(1)
1872 .mount(&mock_server)
1873 .await;
1874 crate::OctocrabBuilder::default()
1875 .base_uri(mock_server.uri())
1876 .unwrap()
1877 .add_header(HeaderName::from_static("x-test1"), "hello".to_string())
1878 .add_header(HeaderName::from_static("x-test2"), "goodbye".to_string())
1879 .build()
1880 .unwrap()
1881 .repos("XAMPPRocky", "octocrab")
1882 .events()
1883 .send()
1884 .await
1885 .unwrap();
1886 }
1887
1888 use super::*;
1889 use chrono::Duration;
1890
1891 #[test]
1892 fn clear_token() {
1893 let cache = CachedToken(RwLock::new(None));
1894 cache.set("secret".to_string(), None);
1895 cache.clear();
1896
1897 assert!(cache.valid_token().is_none(), "Token was not cleared.");
1898 }
1899
1900 #[test]
1901 fn no_token_when_expired() {
1902 let cache = CachedToken(RwLock::new(None));
1903 let expiration = Utc::now() + Duration::seconds(9);
1904 cache.set("secret".to_string(), Some(expiration));
1905
1906 assert!(
1907 cache
1908 .valid_token_with_buffer(Duration::seconds(10))
1909 .is_none(),
1910 "Token should be considered expired due to buffer."
1911 );
1912 }
1913
1914 #[test]
1915 fn get_valid_token_outside_buffer() {
1916 let cache = CachedToken(RwLock::new(None));
1917 let expiration = Utc::now() + Duration::seconds(12);
1918 cache.set("secret".to_string(), Some(expiration));
1919
1920 assert!(
1921 cache
1922 .valid_token_with_buffer(Duration::seconds(10))
1923 .is_some(),
1924 "Token should still be valid outside of buffer."
1925 );
1926 }
1927
1928 #[test]
1929 fn get_valid_token_without_expiration() {
1930 let cache = CachedToken(RwLock::new(None));
1931 cache.set("secret".to_string(), None);
1932
1933 assert!(
1934 cache
1935 .valid_token_with_buffer(Duration::seconds(10))
1936 .is_some(),
1937 "Token with no expiration should always be considered valid."
1938 );
1939 }
1940}