1use crate::auth::admin::AdminApiKey;
32use crate::error::{GhostError, Result};
33use crate::models::pagination::Meta;
34use crate::models::post::{Post, PostCreate, PostCreateEnvelope, PostUpdate, PostUpdateEnvelope};
35use reqwest::{header, Client};
36use serde::{Deserialize, Serialize};
37
38const GHOST_API_VERSION: &str = "v5.0";
39const ADMIN_API_PATH: &str = "/ghost/api/admin";
40
41#[derive(Debug, Deserialize)]
44struct GhostApiErrors {
45 errors: Vec<GhostApiError>,
46}
47
48#[derive(Debug, Deserialize)]
49struct GhostApiError {
50 message: String,
51 #[serde(rename = "type")]
52 error_type: String,
53 context: Option<String>,
54}
55
56#[derive(Debug, Deserialize)]
59struct AdminPostEnvelope {
60 posts: Vec<Post>,
61}
62
63#[derive(Debug, Clone, Default)]
82pub struct AdminBrowsePostsParams {
83 pub page: Option<u32>,
85 pub limit: Option<u32>,
87 pub filter: Option<String>,
89 pub order: Option<String>,
91 pub include: Option<String>,
93 pub fields: Option<String>,
95}
96
97impl AdminBrowsePostsParams {
98 fn to_query_pairs(&self) -> Vec<(&'static str, String)> {
99 let mut pairs: Vec<(&'static str, String)> = Vec::new();
100 if let Some(v) = self.page {
101 pairs.push(("page", v.to_string()));
102 }
103 if let Some(v) = self.limit {
104 pairs.push(("limit", v.to_string()));
105 }
106 if let Some(ref v) = self.filter {
107 pairs.push(("filter", v.clone()));
108 }
109 if let Some(ref v) = self.order {
110 pairs.push(("order", v.clone()));
111 }
112 if let Some(ref v) = self.include {
113 pairs.push(("include", v.clone()));
114 }
115 if let Some(ref v) = self.fields {
116 pairs.push(("fields", v.clone()));
117 }
118 pairs
119 }
120}
121
122#[derive(Debug, Deserialize)]
124pub struct AdminPostsResponse {
125 pub posts: Vec<Post>,
127 pub meta: Meta,
129}
130
131pub struct GhostAdminClient {
160 base_url: String,
161 api_key: AdminApiKey,
162 http: Client,
163}
164
165impl GhostAdminClient {
166 pub fn new(base_url: impl Into<String>, api_key: AdminApiKey) -> Result<Self> {
187 let mut base_url = base_url.into();
188 while base_url.ends_with('/') {
189 base_url.pop();
190 }
191
192 let mut default_headers = header::HeaderMap::new();
193 default_headers.insert(
194 "Accept-Version",
195 header::HeaderValue::from_static(GHOST_API_VERSION),
196 );
197
198 let http = Client::builder()
199 .default_headers(default_headers)
200 .build()
201 .map_err(GhostError::Http)?;
202
203 Ok(Self {
204 base_url,
205 api_key,
206 http,
207 })
208 }
209
210 pub fn base_url(&self) -> &str {
212 &self.base_url
213 }
214
215 pub fn api_key(&self) -> &AdminApiKey {
217 &self.api_key
218 }
219
220 pub async fn browse_posts(&self, params: AdminBrowsePostsParams) -> Result<AdminPostsResponse> {
249 let query = params.to_query_pairs();
250 self.get("/posts/", &query).await
251 }
252
253 pub async fn read_post_by_id(&self, id: &str, include: Option<&str>) -> Result<Post> {
276 let path = format!("/posts/{id}/");
277 let query = include
278 .map(|i| vec![("include", i.to_string())])
279 .unwrap_or_default();
280 let envelope: AdminPostEnvelope = self.get(&path, &query).await?;
281 envelope
282 .posts
283 .into_iter()
284 .next()
285 .ok_or_else(|| GhostError::api("Post not found", "NotFoundError", None))
286 }
287
288 pub async fn read_post_by_slug(&self, slug: &str, include: Option<&str>) -> Result<Post> {
311 let path = format!("/posts/slug/{slug}/");
312 let query = include
313 .map(|i| vec![("include", i.to_string())])
314 .unwrap_or_default();
315 let envelope: AdminPostEnvelope = self.get(&path, &query).await?;
316 envelope
317 .posts
318 .into_iter()
319 .next()
320 .ok_or_else(|| GhostError::api("Post not found", "NotFoundError", None))
321 }
322
323 pub async fn create_post(&self, post: PostCreate) -> Result<Post> {
350 let envelope = PostCreateEnvelope::new(post);
351 let response: AdminPostEnvelope = self.post("/posts/", &envelope).await?;
352 response
353 .posts
354 .into_iter()
355 .next()
356 .ok_or_else(|| GhostError::api("No post in create response", "ServerError", None))
357 }
358
359 pub async fn update_post(&self, id: &str, post: PostUpdate) -> Result<Post> {
391 let path = format!("/posts/{id}/");
392 let envelope = PostUpdateEnvelope::new(post);
393 let response: AdminPostEnvelope = self.put(&path, &envelope).await?;
394 response
395 .posts
396 .into_iter()
397 .next()
398 .ok_or_else(|| GhostError::api("No post in update response", "ServerError", None))
399 }
400
401 pub async fn delete_post(&self, id: &str) -> Result<()> {
423 let path = format!("/posts/{id}/");
424 self.delete(&path).await
425 }
426
427 pub(crate) fn admin_url(&self, path: &str) -> String {
430 format!("{}{}{}", self.base_url, ADMIN_API_PATH, path)
431 }
432
433 pub(crate) fn auth_header_value(&self) -> Result<header::HeaderValue> {
434 let token = self.api_key.generate_jwt()?;
435 let value = format!("Ghost {token}");
436 header::HeaderValue::from_str(&value)
437 .map_err(|e| GhostError::auth(format!("Invalid Authorization header value: {e}")))
438 }
439
440 pub(crate) async fn get<T>(&self, path: &str, query: &[(&str, String)]) -> Result<T>
441 where
442 T: for<'de> Deserialize<'de>,
443 {
444 let url = self.admin_url(path);
445 let auth = self.auth_header_value()?;
446 let response = self
447 .http
448 .get(&url)
449 .header(header::AUTHORIZATION, auth)
450 .query(query)
451 .send()
452 .await?;
453 self.parse_response(response).await
454 }
455
456 pub(crate) async fn post<T, B>(&self, path: &str, body: &B) -> Result<T>
457 where
458 T: for<'de> Deserialize<'de>,
459 B: Serialize,
460 {
461 let url = self.admin_url(path);
462 let auth = self.auth_header_value()?;
463 let response = self
464 .http
465 .post(&url)
466 .header(header::AUTHORIZATION, auth)
467 .json(body)
468 .send()
469 .await?;
470 self.parse_response(response).await
471 }
472
473 pub(crate) async fn put<T, B>(&self, path: &str, body: &B) -> Result<T>
474 where
475 T: for<'de> Deserialize<'de>,
476 B: Serialize,
477 {
478 let url = self.admin_url(path);
479 let auth = self.auth_header_value()?;
480 let response = self
481 .http
482 .put(&url)
483 .header(header::AUTHORIZATION, auth)
484 .json(body)
485 .send()
486 .await?;
487 self.parse_response(response).await
488 }
489
490 pub(crate) async fn delete(&self, path: &str) -> Result<()> {
491 let url = self.admin_url(path);
492 let auth = self.auth_header_value()?;
493 let response = self
494 .http
495 .delete(&url)
496 .header(header::AUTHORIZATION, auth)
497 .send()
498 .await?;
499 if response.status().is_success() {
500 Ok(())
501 } else {
502 let api_errors: GhostApiErrors = response.json().await?;
503 let first = api_errors
504 .errors
505 .into_iter()
506 .next()
507 .unwrap_or(GhostApiError {
508 message: "Unknown API error".to_string(),
509 error_type: "UnknownError".to_string(),
510 context: None,
511 });
512 Err(GhostError::api(
513 first.message,
514 first.error_type,
515 first.context,
516 ))
517 }
518 }
519
520 async fn parse_response<T>(&self, response: reqwest::Response) -> Result<T>
521 where
522 T: for<'de> Deserialize<'de>,
523 {
524 let status = response.status();
525 if status.is_success() {
526 Ok(response.json::<T>().await?)
527 } else {
528 let api_errors: GhostApiErrors = response.json().await?;
529 let first = api_errors
530 .errors
531 .into_iter()
532 .next()
533 .unwrap_or(GhostApiError {
534 message: "Unknown API error".to_string(),
535 error_type: "UnknownError".to_string(),
536 context: None,
537 });
538 Err(GhostError::api(
539 first.message,
540 first.error_type,
541 first.context,
542 ))
543 }
544 }
545}
546
547#[cfg(test)]
550mod tests {
551 use super::*;
552 use wiremock::matchers::{body_json, header_exists, method, path, query_param};
553 use wiremock::{Mock, MockServer, ResponseTemplate};
554
555 const VALID_KEY: &str =
556 "6748592f4b9b7700010f6564:b1b5b9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1";
557
558 fn make_key() -> AdminApiKey {
559 AdminApiKey::new(VALID_KEY).unwrap()
560 }
561
562 fn make_client(uri: &str) -> GhostAdminClient {
563 GhostAdminClient::new(uri, make_key()).unwrap()
564 }
565
566 fn post_json(id: &str, title: &str, status: &str) -> serde_json::Value {
567 serde_json::json!({
568 "id": id,
569 "title": title,
570 "slug": title.to_lowercase().replace(' ', "-"),
571 "status": status,
572 "updated_at": "2026-01-01T00:00:00.000Z"
573 })
574 }
575
576 fn pagination_meta(page: u32, total: u32) -> serde_json::Value {
577 serde_json::json!({
578 "pagination": {
579 "page": page, "limit": 15,
580 "pages": 1, "total": total,
581 "next": null, "prev": null
582 }
583 })
584 }
585
586 #[test]
589 fn test_client_creation() {
590 assert!(GhostAdminClient::new("https://example.ghost.io", make_key()).is_ok());
591 }
592
593 #[test]
594 fn test_client_strips_trailing_slash() {
595 let c = GhostAdminClient::new("https://example.ghost.io/", make_key()).unwrap();
596 assert_eq!(c.base_url(), "https://example.ghost.io");
597 }
598
599 #[test]
602 fn test_browse_params_default_empty() {
603 assert!(AdminBrowsePostsParams::default()
604 .to_query_pairs()
605 .is_empty());
606 }
607
608 #[test]
609 fn test_browse_params_all_fields() {
610 let p = AdminBrowsePostsParams {
611 page: Some(2),
612 limit: Some(5),
613 filter: Some("status:draft".to_string()),
614 order: Some("published_at DESC".to_string()),
615 include: Some("authors,tags".to_string()),
616 fields: Some("id,title".to_string()),
617 };
618 let pairs = p.to_query_pairs();
619 let map: std::collections::HashMap<_, _> = pairs.into_iter().collect();
620 assert_eq!(map["page"], "2");
621 assert_eq!(map["limit"], "5");
622 assert_eq!(map["filter"], "status:draft");
623 assert_eq!(map["order"], "published_at DESC");
624 assert_eq!(map["include"], "authors,tags");
625 assert_eq!(map["fields"], "id,title");
626 }
627
628 #[test]
629 fn test_browse_params_partial() {
630 let p = AdminBrowsePostsParams {
631 limit: Some(10),
632 ..Default::default()
633 };
634 let pairs = p.to_query_pairs();
635 assert_eq!(pairs.len(), 1);
636 assert_eq!(pairs[0], ("limit", "10".to_string()));
637 }
638
639 #[tokio::test]
642 async fn test_browse_posts_returns_posts_and_meta() {
643 let server = MockServer::start().await;
644 Mock::given(method("GET"))
645 .and(path("/ghost/api/admin/posts/"))
646 .and(header_exists("Authorization"))
647 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
648 "posts": [post_json("id1", "First Post", "draft")],
649 "meta": pagination_meta(1, 1)
650 })))
651 .mount(&server)
652 .await;
653
654 let response = make_client(&server.uri())
655 .browse_posts(AdminBrowsePostsParams::default())
656 .await
657 .unwrap();
658
659 assert_eq!(response.posts.len(), 1);
660 assert_eq!(response.posts[0].id, "id1");
661 assert_eq!(response.posts[0].title, "First Post");
662 assert_eq!(response.meta.pagination.total, 1);
663 }
664
665 #[tokio::test]
666 async fn test_browse_posts_forwards_query_params() {
667 let server = MockServer::start().await;
668 Mock::given(method("GET"))
669 .and(path("/ghost/api/admin/posts/"))
670 .and(query_param("limit", "5"))
671 .and(query_param("filter", "status:draft"))
672 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
673 "posts": [],
674 "meta": pagination_meta(1, 0)
675 })))
676 .expect(1)
677 .mount(&server)
678 .await;
679
680 make_client(&server.uri())
681 .browse_posts(AdminBrowsePostsParams {
682 limit: Some(5),
683 filter: Some("status:draft".to_string()),
684 ..Default::default()
685 })
686 .await
687 .unwrap();
688
689 server.verify().await;
690 }
691
692 #[tokio::test]
693 async fn test_browse_posts_empty_result() {
694 let server = MockServer::start().await;
695 Mock::given(method("GET"))
696 .and(path("/ghost/api/admin/posts/"))
697 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
698 "posts": [],
699 "meta": pagination_meta(1, 0)
700 })))
701 .mount(&server)
702 .await;
703
704 let response = make_client(&server.uri())
705 .browse_posts(AdminBrowsePostsParams::default())
706 .await
707 .unwrap();
708
709 assert!(response.posts.is_empty());
710 assert_eq!(response.meta.pagination.total, 0);
711 }
712
713 #[tokio::test]
716 async fn test_read_post_by_id_success() {
717 let server = MockServer::start().await;
718 Mock::given(method("GET"))
719 .and(path("/ghost/api/admin/posts/abc123/"))
720 .and(header_exists("Authorization"))
721 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
722 "posts": [post_json("abc123", "Test Post", "published")]
723 })))
724 .mount(&server)
725 .await;
726
727 let post = make_client(&server.uri())
728 .read_post_by_id("abc123", None)
729 .await
730 .unwrap();
731
732 assert_eq!(post.id, "abc123");
733 assert_eq!(post.title, "Test Post");
734 }
735
736 #[tokio::test]
737 async fn test_read_post_by_id_with_include() {
738 let server = MockServer::start().await;
739 Mock::given(method("GET"))
740 .and(path("/ghost/api/admin/posts/abc123/"))
741 .and(query_param("include", "authors,tags"))
742 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
743 "posts": [post_json("abc123", "Test Post", "draft")]
744 })))
745 .expect(1)
746 .mount(&server)
747 .await;
748
749 make_client(&server.uri())
750 .read_post_by_id("abc123", Some("authors,tags"))
751 .await
752 .unwrap();
753
754 server.verify().await;
755 }
756
757 #[tokio::test]
758 async fn test_read_post_by_id_not_found() {
759 let server = MockServer::start().await;
760 Mock::given(method("GET"))
761 .and(path("/ghost/api/admin/posts/nope/"))
762 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
763 "errors": [{"message": "Post not found", "type": "NotFoundError", "context": null}]
764 })))
765 .mount(&server)
766 .await;
767
768 let err = make_client(&server.uri())
769 .read_post_by_id("nope", None)
770 .await
771 .unwrap_err();
772
773 assert!(err.is_api_error());
774 assert_eq!(err.api_error_type(), Some("NotFoundError"));
775 }
776
777 #[tokio::test]
780 async fn test_read_post_by_slug_success() {
781 let server = MockServer::start().await;
782 Mock::given(method("GET"))
783 .and(path("/ghost/api/admin/posts/slug/my-post/"))
784 .and(header_exists("Authorization"))
785 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
786 "posts": [post_json("abc123", "My Post", "published")]
787 })))
788 .mount(&server)
789 .await;
790
791 let post = make_client(&server.uri())
792 .read_post_by_slug("my-post", None)
793 .await
794 .unwrap();
795
796 assert_eq!(post.id, "abc123");
797 assert_eq!(post.slug, "my-post");
798 }
799
800 #[tokio::test]
801 async fn test_read_post_by_slug_with_include() {
802 let server = MockServer::start().await;
803 Mock::given(method("GET"))
804 .and(path("/ghost/api/admin/posts/slug/hello/"))
805 .and(query_param("include", "authors"))
806 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
807 "posts": [post_json("id1", "Hello", "draft")]
808 })))
809 .expect(1)
810 .mount(&server)
811 .await;
812
813 make_client(&server.uri())
814 .read_post_by_slug("hello", Some("authors"))
815 .await
816 .unwrap();
817
818 server.verify().await;
819 }
820
821 #[tokio::test]
822 async fn test_read_post_by_slug_not_found() {
823 let server = MockServer::start().await;
824 Mock::given(method("GET"))
825 .and(path("/ghost/api/admin/posts/slug/nope/"))
826 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
827 "errors": [{"message": "Post not found", "type": "NotFoundError", "context": null}]
828 })))
829 .mount(&server)
830 .await;
831
832 let err = make_client(&server.uri())
833 .read_post_by_slug("nope", None)
834 .await
835 .unwrap_err();
836
837 assert_eq!(err.api_error_type(), Some("NotFoundError"));
838 }
839
840 #[tokio::test]
843 async fn test_create_post_sends_envelope() {
844 let server = MockServer::start().await;
845
846 let expected_body = serde_json::json!({
847 "posts": [{"title": "New Post"}]
848 });
849
850 Mock::given(method("POST"))
851 .and(path("/ghost/api/admin/posts/"))
852 .and(header_exists("Authorization"))
853 .and(body_json(&expected_body))
854 .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
855 "posts": [post_json("new-id", "New Post", "draft")]
856 })))
857 .expect(1)
858 .mount(&server)
859 .await;
860
861 let post = make_client(&server.uri())
862 .create_post(PostCreate::new("New Post"))
863 .await
864 .unwrap();
865
866 assert_eq!(post.id, "new-id");
867 assert_eq!(post.title, "New Post");
868 server.verify().await;
869 }
870
871 #[tokio::test]
872 async fn test_create_post_with_status() {
873 let server = MockServer::start().await;
874
875 let expected_body = serde_json::json!({
876 "posts": [{"title": "Published", "status": "published"}]
877 });
878
879 Mock::given(method("POST"))
880 .and(path("/ghost/api/admin/posts/"))
881 .and(body_json(&expected_body))
882 .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
883 "posts": [post_json("pub-id", "Published", "published")]
884 })))
885 .mount(&server)
886 .await;
887
888 use crate::models::post::PostStatus;
889 let post = make_client(&server.uri())
890 .create_post(PostCreate {
891 title: "Published".to_string(),
892 status: Some(PostStatus::Published),
893 ..Default::default()
894 })
895 .await
896 .unwrap();
897
898 assert_eq!(post.id, "pub-id");
899 }
900
901 #[tokio::test]
902 async fn test_create_post_api_error() {
903 let server = MockServer::start().await;
904 Mock::given(method("POST"))
905 .and(path("/ghost/api/admin/posts/"))
906 .respond_with(ResponseTemplate::new(422).set_body_json(serde_json::json!({
907 "errors": [{"message": "Validation failed", "type": "ValidationError", "context": "Title is required"}]
908 })))
909 .mount(&server)
910 .await;
911
912 let err = make_client(&server.uri())
913 .create_post(PostCreate::new(""))
914 .await
915 .unwrap_err();
916
917 assert!(err.is_api_error());
918 assert_eq!(err.api_error_type(), Some("ValidationError"));
919 assert_eq!(err.api_message(), Some("Validation failed"));
920 }
921
922 #[tokio::test]
925 async fn test_update_post_sends_envelope_with_updated_at() {
926 let server = MockServer::start().await;
927
928 let expected_body = serde_json::json!({
929 "posts": [{"updated_at": "2026-01-01T00:00:00.000Z"}]
930 });
931
932 Mock::given(method("PUT"))
933 .and(path("/ghost/api/admin/posts/abc123/"))
934 .and(header_exists("Authorization"))
935 .and(body_json(&expected_body))
936 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
937 "posts": [post_json("abc123", "Unchanged", "draft")]
938 })))
939 .expect(1)
940 .mount(&server)
941 .await;
942
943 make_client(&server.uri())
944 .update_post("abc123", PostUpdate::new("2026-01-01T00:00:00.000Z"))
945 .await
946 .unwrap();
947
948 server.verify().await;
949 }
950
951 #[tokio::test]
952 async fn test_update_post_returns_updated_post() {
953 let server = MockServer::start().await;
954
955 Mock::given(method("PUT"))
956 .and(path("/ghost/api/admin/posts/abc123/"))
957 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
958 "posts": [post_json("abc123", "Updated Title", "published")]
959 })))
960 .mount(&server)
961 .await;
962
963 use crate::models::post::PostStatus;
964 let post = make_client(&server.uri())
965 .update_post(
966 "abc123",
967 PostUpdate {
968 updated_at: "2026-01-01T00:00:00.000Z".to_string(),
969 title: Some("Updated Title".to_string()),
970 status: Some(PostStatus::Published),
971 ..Default::default()
972 },
973 )
974 .await
975 .unwrap();
976
977 assert_eq!(post.title, "Updated Title");
978 }
979
980 #[tokio::test]
981 async fn test_update_post_conflict_error() {
982 let server = MockServer::start().await;
983 Mock::given(method("PUT"))
984 .and(path("/ghost/api/admin/posts/abc123/"))
985 .respond_with(ResponseTemplate::new(409).set_body_json(serde_json::json!({
986 "errors": [{"message": "Conflict", "type": "ConflictError", "context": "updated_at is stale"}]
987 })))
988 .mount(&server)
989 .await;
990
991 let err = make_client(&server.uri())
992 .update_post("abc123", PostUpdate::new("2020-01-01T00:00:00.000Z"))
993 .await
994 .unwrap_err();
995
996 assert_eq!(err.api_error_type(), Some("ConflictError"));
997 }
998
999 #[tokio::test]
1000 async fn test_update_post_not_found() {
1001 let server = MockServer::start().await;
1002 Mock::given(method("PUT"))
1003 .and(path("/ghost/api/admin/posts/nope/"))
1004 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1005 "errors": [{"message": "Not found", "type": "NotFoundError", "context": null}]
1006 })))
1007 .mount(&server)
1008 .await;
1009
1010 let err = make_client(&server.uri())
1011 .update_post("nope", PostUpdate::new("2026-01-01T00:00:00.000Z"))
1012 .await
1013 .unwrap_err();
1014
1015 assert_eq!(err.api_error_type(), Some("NotFoundError"));
1016 }
1017
1018 #[tokio::test]
1021 async fn test_delete_post_success() {
1022 let server = MockServer::start().await;
1023 Mock::given(method("DELETE"))
1024 .and(path("/ghost/api/admin/posts/abc123/"))
1025 .and(header_exists("Authorization"))
1026 .respond_with(ResponseTemplate::new(204))
1027 .expect(1)
1028 .mount(&server)
1029 .await;
1030
1031 make_client(&server.uri())
1032 .delete_post("abc123")
1033 .await
1034 .unwrap();
1035
1036 server.verify().await;
1037 }
1038
1039 #[tokio::test]
1040 async fn test_delete_post_not_found() {
1041 let server = MockServer::start().await;
1042 Mock::given(method("DELETE"))
1043 .and(path("/ghost/api/admin/posts/nope/"))
1044 .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
1045 "errors": [{"message": "Post not found", "type": "NotFoundError", "context": null}]
1046 })))
1047 .mount(&server)
1048 .await;
1049
1050 let err = make_client(&server.uri())
1051 .delete_post("nope")
1052 .await
1053 .unwrap_err();
1054
1055 assert_eq!(err.api_error_type(), Some("NotFoundError"));
1056 }
1057
1058 #[test]
1061 fn test_auth_header_starts_with_ghost() {
1062 let client = GhostAdminClient::new("https://example.ghost.io", make_key()).unwrap();
1063 let hv = client.auth_header_value().unwrap();
1064 assert!(hv.to_str().unwrap().starts_with("Ghost "));
1065 }
1066
1067 #[test]
1068 fn test_admin_url_construction() {
1069 let client = GhostAdminClient::new("https://example.ghost.io", make_key()).unwrap();
1070 assert_eq!(
1071 client.admin_url("/posts/"),
1072 "https://example.ghost.io/ghost/api/admin/posts/"
1073 );
1074 }
1075}