diff options
author | Cyborus <cyborus@noreply.codeberg.org> | 2024-05-31 17:24:42 +0200 |
---|---|---|
committer | Cyborus <cyborus@noreply.codeberg.org> | 2024-05-31 17:24:42 +0200 |
commit | 7fa8e0acb764e1f819ca082ad7b3b133b2882e82 (patch) | |
tree | bf27bb484e07e82d6db78eef9183eb99118be926 | |
parent | add oauth2 testing (diff) | |
parent | add `/test_repos` to gitignore (diff) | |
download | forgejo-api-7fa8e0acb764e1f819ca082ad7b3b133b2882e82.tar.xz forgejo-api-7fa8e0acb764e1f819ca082ad7b3b133b2882e82.zip |
Merge pull request 'improve integration tests' (#56) from improve-tests into main
Reviewed-on: https://codeberg.org/Cyborus/forgejo-api/pulls/56
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | src/lib.rs | 10 | ||||
-rw-r--r-- | tests/admin.rs | 194 | ||||
-rw-r--r-- | tests/ci_test.rs | 535 | ||||
-rw-r--r-- | tests/common/mod.rs | 17 | ||||
-rw-r--r-- | tests/repo.rs | 276 | ||||
-rw-r--r-- | tests/user.rs | 193 |
7 files changed, 687 insertions, 539 deletions
@@ -1 +1,2 @@ /target +/test_repos @@ -99,11 +99,13 @@ impl Forgejo { password, mfa, } => { - let len = (((username.len() + password.len() + 1) + let unencoded_len = username.len() + password.len() + 1; + let unpadded_len = unencoded_len .checked_mul(4) - .ok_or(ForgejoError::AuthTooLong)?) - / 3) - + 1; + .ok_or(ForgejoError::AuthTooLong)? + .div_ceil(3); + // round up to next multiple of 4, to account for padding + let len = unpadded_len.div_ceil(4) * 4; let mut bytes = vec![0; len]; // panic safety: len cannot be zero diff --git a/tests/admin.rs b/tests/admin.rs new file mode 100644 index 0000000..06aed66 --- /dev/null +++ b/tests/admin.rs @@ -0,0 +1,194 @@ +use forgejo_api::structs::*; + +mod common; + +#[tokio::test] +async fn user() { + let api = common::login(); + + let user_opt = CreateUserOption { + created_at: None, + email: "pipis@noreply.example.org".into(), + full_name: None, + login_name: None, + must_change_password: None, + password: Some("userpass".into()), + restricted: Some(false), + send_notify: Some(true), + source_id: None, + username: "Pipis".into(), + visibility: Some("public".into()), + }; + let _ = api + .admin_create_user(user_opt) + .await + .expect("failed to create user"); + + let query = AdminSearchUsersQuery::default(); + let users = api + .admin_search_users(query) + .await + .expect("failed to search users"); + assert!( + users + .iter() + .find(|u| u.login.as_ref().unwrap() == "Pipis") + .is_some(), + "could not find new user" + ); + let query = AdminGetAllEmailsQuery::default(); + let users = api + .admin_get_all_emails(query) + .await + .expect("failed to search emails"); + assert!( + users + .iter() + .find(|u| u.email.as_ref().unwrap() == "pipis@noreply.example.org") + .is_some(), + "could not find new user" + ); +} + +#[tokio::test] +async fn org() { + let api = common::login(); + + let user_opt = CreateUserOption { + created_at: None, + email: "org-owner@noreply.example.org".into(), + full_name: None, + login_name: None, + must_change_password: None, + password: Some("userpass".into()), + restricted: Some(false), + send_notify: Some(true), + source_id: None, + username: "OrgOwner".into(), + visibility: Some("public".into()), + }; + let _ = api + .admin_create_user(user_opt) + .await + .expect("failed to create user"); + + let org_opt = CreateOrgOption { + description: None, + email: None, + full_name: None, + location: None, + repo_admin_change_team_access: None, + username: "test-org".into(), + visibility: Some(CreateOrgOptionVisibility::Public), + website: None, + }; + let _ = api + .admin_create_org("OrgOwner", org_opt) + .await + .expect("failed to create org"); + let query = AdminGetAllOrgsQuery::default(); + assert!( + !api.admin_get_all_orgs(query).await.unwrap().is_empty(), + "org list empty" + ); + let rename_opt = RenameUserOption { + new_username: "Bepis".into(), + }; + api.admin_rename_user("Pipis", rename_opt) + .await + .expect("failed to rename user"); + let query = AdminDeleteUserQuery { purge: Some(true) }; + api.admin_delete_user("Bepis", query) + .await + .expect("failed to delete user"); + let query = AdminDeleteUserQuery { purge: Some(true) }; + assert!( + api.admin_delete_user("Ghost", query).await.is_err(), + "deleting fake user should fail" + ); +} + +#[tokio::test] +async fn key() { + let api = common::login(); + + let user_opt = CreateUserOption { + created_at: None, + email: "key-holder@noreply.example.org".into(), + full_name: None, + login_name: None, + must_change_password: None, + password: Some("userpass".into()), + restricted: Some(false), + send_notify: Some(true), + source_id: None, + username: "KeyHolder".into(), + visibility: Some("public".into()), + }; + let _ = api + .admin_create_user(user_opt) + .await + .expect("failed to create user"); + + let key_opt = CreateKeyOption { + key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN68ehQAsbGEwlXPa2AxbAh1QxFQrtRel2jeC0hRlPc1 user@noreply.example.org".into(), + read_only: None, + title: "Example Key".into(), + }; + let key = api + .admin_create_public_key("KeyHolder", key_opt) + .await + .expect("failed to create key"); + api.admin_delete_user_public_key("KeyHolder", key.id.unwrap()) + .await + .expect("failed to delete key"); +} + +#[tokio::test] +async fn cron() { + let api = common::login(); + + let query = AdminCronListQuery::default(); + let crons = api + .admin_cron_list(query) + .await + .expect("failed to get crons list"); + api.admin_cron_run(&crons.get(0).expect("no crons").name.as_ref().unwrap()) + .await + .expect("failed to run cron"); +} + +#[tokio::test] +async fn hook() { + let api = common::login(); + let hook_opt = CreateHookOption { + active: None, + authorization_header: None, + branch_filter: None, + config: CreateHookOptionConfig { + content_type: "json".into(), + url: url::Url::parse("http://test.local/").unwrap(), + additional: Default::default(), + }, + events: Some(Vec::new()), + r#type: CreateHookOptionType::Gitea, + }; + // yarr har har me matey this is me hook + let hook = api + .admin_create_hook(hook_opt) + .await + .expect("failed to create hook"); + let edit_hook = EditHookOption { + active: Some(true), + authorization_header: None, + branch_filter: None, + config: None, + events: None, + }; + api.admin_edit_hook(hook.id.unwrap(), edit_hook) + .await + .expect("failed to edit hook"); + api.admin_delete_hook(hook.id.unwrap()) + .await + .expect("failed to delete hook"); +} diff --git a/tests/ci_test.rs b/tests/ci_test.rs deleted file mode 100644 index 269c522..0000000 --- a/tests/ci_test.rs +++ /dev/null @@ -1,535 +0,0 @@ -use forgejo_api::{structs::*, Forgejo}; - -fn get_api() -> Forgejo { - let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap(); - let token = std::env::var("FORGEJO_API_CI_TOKEN").unwrap(); - Forgejo::new(forgejo_api::Auth::Token(&token), url).unwrap() -} - -#[tokio::test] -async fn user() { - let api = get_api(); - - let myself = api.user_get_current().await.unwrap(); - assert!(myself.is_admin.unwrap(), "user should be admin"); - assert_eq!( - myself.login.as_ref().unwrap(), - "TestingAdmin", - "user should be named \"TestingAdmin\"" - ); - - let myself_indirect = api.user_get("TestingAdmin").await.unwrap(); - assert_eq!( - myself, myself_indirect, - "result of `myself` does not match result of `get_user`" - ); - - let query = UserListFollowingQuery::default(); - let following = api - .user_list_following("TestingAdmin", query) - .await - .unwrap(); - assert_eq!(following, Vec::new(), "following list not empty"); - - let query = UserListFollowersQuery::default(); - let followers = api - .user_list_followers("TestingAdmin", query) - .await - .unwrap(); - assert_eq!(followers, Vec::new(), "follower list not empty"); - - let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap(); - let password_api = Forgejo::new( - forgejo_api::Auth::Password { - username: "TestingAdmin", - password: "password", - mfa: None, - }, - url, - ) - .expect("failed to log in using username and password"); - - assert!( - api.user_get_current().await.unwrap() == password_api.user_get_current().await.unwrap(), - "users not equal comparing token-auth and pass-auth" - ); -} - -#[tokio::test] -async fn repo() { - let api = get_api(); - - tokio::fs::create_dir("./test_repo").await.unwrap(); - let git = || { - let mut cmd = std::process::Command::new("git"); - cmd.current_dir("./test_repo"); - cmd - }; - let _ = git() - .args(["config", "--global", "init.defaultBranch", "main"]) - .status() - .unwrap(); - let _ = git().args(["init"]).status().unwrap(); - let _ = git() - .args(["config", "user.name", "TestingAdmin"]) - .status() - .unwrap(); - let _ = git() - .args(["config", "user.email", "admin@noreply.example.org"]) - .status() - .unwrap(); - tokio::fs::write("./test_repo/README.md", "# Test\nThis is a test repo") - .await - .unwrap(); - let _ = git().args(["add", "."]).status().unwrap(); - let _ = git() - .args(["commit", "-m", "initial commit"]) - .status() - .unwrap(); - - let repo_opt = CreateRepoOption { - auto_init: Some(false), - default_branch: Some("main".into()), - description: Some("Test Repo".into()), - gitignores: Some("".into()), - issue_labels: Some("".into()), - license: Some("".into()), - name: "test".into(), - object_format_name: None, - private: Some(false), - readme: None, - template: Some(false), - trust_model: Some(CreateRepoOptionTrustModel::Default), - }; - let remote_repo = api.create_current_user_repo(repo_opt).await.unwrap(); - assert!( - remote_repo.has_pull_requests.unwrap(), - "repo does not accept pull requests" - ); - assert!( - remote_repo.owner.as_ref().unwrap().login.as_ref().unwrap() == "TestingAdmin", - "repo owner is not \"TestingAdmin\"" - ); - assert!( - remote_repo.name.as_ref().unwrap() == "test", - "repo owner is not \"test\"" - ); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - - let mut remote_url = remote_repo.clone_url.clone().unwrap(); - remote_url.set_username("TestingAdmin").unwrap(); - remote_url.set_password(Some("password")).unwrap(); - let _ = git() - .args(["remote", "add", "origin", remote_url.as_str()]) - .status() - .unwrap(); - let _ = git() - .args(["push", "-u", "origin", "main"]) - .status() - .unwrap(); - - let _ = git().args(["switch", "-c", "test"]).status().unwrap(); - tokio::fs::write( - "./test_repo/example.rs", - "fn add_one(x: u32) -> u32 { x + 1 }", - ) - .await - .unwrap(); - let _ = git().args(["add", "."]).status().unwrap(); - let _ = git().args(["commit", "-m", "egg"]).status().unwrap(); - let _ = git() - .args(["push", "-u", "origin", "test"]) - .status() - .unwrap(); - - let pr_opt = CreatePullRequestOption { - assignee: None, - assignees: Some(vec!["TestingAdmin".into()]), - base: Some("main".into()), - body: Some("This is a test PR".into()), - due_date: None, - head: Some("test".into()), - labels: None, - milestone: None, - title: Some("test pr".into()), - }; - let pr = api - .repo_create_pull_request("TestingAdmin", "test", pr_opt) - .await - .expect("couldn't create pr"); - tokio::time::sleep(std::time::Duration::from_secs(3)).await; - let is_merged = api - .repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap()) - .await - .is_ok(); - assert!(!is_merged, "pr should not yet be merged"); - let pr_files_query = RepoGetPullRequestFilesQuery::default(); - let (_, _) = api - .repo_get_pull_request_files("TestingAdmin", "test", pr.number.unwrap(), pr_files_query) - .await - .unwrap(); - let merge_opt = MergePullRequestOption { - r#do: MergePullRequestOptionDo::Merge, - merge_commit_id: None, - merge_message_field: None, - merge_title_field: None, - delete_branch_after_merge: Some(true), - force_merge: None, - head_commit_id: None, - merge_when_checks_succeed: None, - }; - api.repo_merge_pull_request("TestingAdmin", "test", pr.number.unwrap(), merge_opt) - .await - .expect("couldn't merge pr"); - let is_merged = api - .repo_pull_request_is_merged("TestingAdmin", "test", pr.number.unwrap()) - .await - .is_ok(); - assert!(is_merged, "pr should be merged"); - let _ = git().args(["fetch"]).status().unwrap(); - let _ = git().args(["pull"]).status().unwrap(); - - let query = RepoListReleasesQuery::default(); - assert!( - api.repo_list_releases("TestingAdmin", "test", query) - .await - .unwrap() - .is_empty(), - "there should be no releases yet" - ); - - let tag_opt = CreateTagOption { - message: Some("This is a tag!".into()), - tag_name: "v1.0".into(), - target: None, - }; - api.repo_create_tag("TestingAdmin", "test", tag_opt) - .await - .expect("failed to create tag"); - - let release_opt = CreateReleaseOption { - body: Some("This is a release!".into()), - draft: Some(true), - name: Some("v1.0".into()), - prerelease: Some(false), - tag_name: "v1.0".into(), - target_commitish: None, - }; - let release = api - .repo_create_release("TestingAdmin", "test", release_opt) - .await - .expect("failed to create release"); - let edit_release = EditReleaseOption { - body: None, - draft: Some(false), - name: None, - prerelease: None, - tag_name: None, - target_commitish: None, - }; - api.repo_edit_release("TestingAdmin", "test", release.id.unwrap(), edit_release) - .await - .expect("failed to edit release"); - - let release_by_tag = api - .repo_get_release_by_tag("TestingAdmin", "test", "v1.0") - .await - .expect("failed to find release"); - let release_latest = api - .repo_get_latest_release("TestingAdmin", "test") - .await - .expect("failed to find latest release"); - assert!(release_by_tag == release_latest, "releases not equal"); - - let attachment = api - .repo_create_release_attachment( - "TestingAdmin", - "test", - release.id.unwrap(), - b"This is a file!".to_vec(), - RepoCreateReleaseAttachmentQuery { - name: Some("test.txt".into()), - }, - ) - .await - .expect("failed to create release attachment"); - assert!( - &*api - .download_release_attachment( - "TestingAdmin", - "test", - release.id.unwrap(), - attachment.id.unwrap() - ) - .await - .unwrap() - == b"This is a file!", - "couldn't download attachment" - ); - let _zip_archive = api - .repo_get_archive("TestingAdmin", "test", "v1.0.zip") - .await - .unwrap(); - let _tar_archive = api - .repo_get_archive("TestingAdmin", "test", "v1.0.tar.gz") - .await - .unwrap(); - // check these contents when their return value is fixed - - api.repo_delete_release_attachment( - "TestingAdmin", - "test", - release.id.unwrap(), - attachment.id.unwrap(), - ) - .await - .expect("failed to deleted attachment"); - - api.repo_delete_release("TestingAdmin", "test", release.id.unwrap()) - .await - .expect("failed to delete release"); - - api.repo_delete_tag("TestingAdmin", "test", "v1.0") - .await - .expect("failed to delete release"); -} - -#[tokio::test] -async fn admin() { - let api = get_api(); - - let user_opt = CreateUserOption { - created_at: None, - email: "pipis@noreply.example.org".into(), - full_name: None, - login_name: None, - must_change_password: None, - password: Some("userpass".into()), - restricted: Some(false), - send_notify: Some(true), - source_id: None, - username: "Pipis".into(), - visibility: Some("public".into()), - }; - let _ = api - .admin_create_user(user_opt) - .await - .expect("failed to create user"); - - let query = AdminSearchUsersQuery::default(); - let users = api - .admin_search_users(query) - .await - .expect("failed to search users"); - assert!( - users - .iter() - .find(|u| u.login.as_ref().unwrap() == "Pipis") - .is_some(), - "could not find new user" - ); - let query = AdminGetAllEmailsQuery::default(); - let users = api - .admin_get_all_emails(query) - .await - .expect("failed to search emails"); - assert!( - users - .iter() - .find(|u| u.email.as_ref().unwrap() == "pipis@noreply.example.org") - .is_some(), - "could not find new user" - ); - - let org_opt = CreateOrgOption { - description: None, - email: None, - full_name: None, - location: None, - repo_admin_change_team_access: None, - username: "test-org".into(), - visibility: Some(CreateOrgOptionVisibility::Public), - website: None, - }; - let _ = api - .admin_create_org("Pipis", org_opt) - .await - .expect("failed to create org"); - let query = AdminGetAllOrgsQuery::default(); - assert!( - !api.admin_get_all_orgs(query).await.unwrap().is_empty(), - "org list empty" - ); - - let key_opt = CreateKeyOption { - key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN68ehQAsbGEwlXPa2AxbAh1QxFQrtRel2jeC0hRlPc1 user@noreply.example.org".into(), - read_only: None, - title: "Example Key".into(), - }; - let key = api - .admin_create_public_key("Pipis", key_opt) - .await - .expect("failed to create key"); - api.admin_delete_user_public_key("Pipis", key.id.unwrap()) - .await - .expect("failed to delete key"); - - let rename_opt = RenameUserOption { - new_username: "Bepis".into(), - }; - api.admin_rename_user("Pipis", rename_opt) - .await - .expect("failed to rename user"); - let query = AdminDeleteUserQuery { purge: Some(true) }; - api.admin_delete_user("Bepis", query) - .await - .expect("failed to delete user"); - let query = AdminDeleteUserQuery { purge: Some(true) }; - assert!( - api.admin_delete_user("Ghost", query).await.is_err(), - "deleting fake user should fail" - ); - - let query = AdminCronListQuery::default(); - let crons = api - .admin_cron_list(query) - .await - .expect("failed to get crons list"); - api.admin_cron_run(&crons.get(0).expect("no crons").name.as_ref().unwrap()) - .await - .expect("failed to run cron"); - - let hook_opt = CreateHookOption { - active: None, - authorization_header: None, - branch_filter: None, - config: CreateHookOptionConfig { - content_type: "json".into(), - url: url::Url::parse("http://test.local/").unwrap(), - additional: Default::default(), - }, - events: Some(Vec::new()), - r#type: CreateHookOptionType::Gitea, - }; - // yarr har har me matey this is me hook - let hook = api - .admin_create_hook(hook_opt) - .await - .expect("failed to create hook"); - let edit_hook = EditHookOption { - active: Some(true), - authorization_header: None, - branch_filter: None, - config: None, - events: None, - }; - api.admin_edit_hook(hook.id.unwrap(), edit_hook) - .await - .expect("failed to edit hook"); - api.admin_delete_hook(hook.id.unwrap()) - .await - .expect("failed to delete hook"); -} - -#[tokio::test] -async fn oauth2_login() { - let api = get_api(); - let opt = forgejo_api::structs::CreateOAuth2ApplicationOptions { - confidential_client: Some(true), - name: Some("Test Application".into()), - redirect_uris: Some(vec!["http://127.0.0.1:48879/".into()]), - }; - let app = api.user_create_oauth2_application(opt).await.unwrap(); - let client_id = app.client_id.unwrap(); - let client_secret = app.client_secret.unwrap(); - - let base_url = &std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap(); - - let client = reqwest::Client::builder() - .cookie_store(true) - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - // Log in via the web interface - let _ = client - .post(&format!("{base_url}user/login")) - .form(&[("user_name", "TestingAdmin"), ("password", "password")]) - .send() - .await - .unwrap() - .error_for_status() - .unwrap(); - - // Load the authorization page - let response = client - .get(&format!( - "{base_url}login/oauth/authorize\ - ?client_id={client_id}\ - &redirect_uri=http%3A%2F%2F127.0.0.1%3A48879%2F\ - &response_type=code\ - &state=theyve" - )) - .send() - .await - .unwrap() - .error_for_status() - .unwrap(); - let csrf = response.cookies().find(|x| x.name() == "_csrf").unwrap(); - - // Authorize the new application via the web interface - let response = client - .post(&format!("{base_url}login/oauth/grant")) - .form(&[ - ("_csrf", csrf.value()), - ("client_id", &client_id), - ("state", "theyve"), - ("scope", ""), - ("nonce", ""), - ("redirect_uri", "http://127.0.0.1:48879/"), - ]) - .send() - .await - .unwrap() - .error_for_status() - .unwrap(); - - // Extract the code from the redirect url - let location = response.headers().get(reqwest::header::LOCATION).unwrap(); - let location = url::Url::parse(dbg!(location.to_str().unwrap())).unwrap(); - let mut code = None; - for (key, value) in location.query_pairs() { - if key == "code" { - code = Some(value.into_owned()); - } else if key == "error_description" { - panic!("{value}"); - } - } - let code = code.unwrap(); - - // Redeem the code and check it works - let url = url::Url::parse(&base_url).unwrap(); - let api = Forgejo::new(forgejo_api::Auth::None, url.clone()).unwrap(); - - let request = forgejo_api::structs::OAuthTokenRequest::Confidential { - client_id: &client_id, - client_secret: &client_secret, - code: &code, - redirect_uri: url::Url::parse("http://127.0.0.1:48879/").unwrap(), - }; - let token = api.oauth_get_access_token(request).await.unwrap(); - let token_api = - Forgejo::new(forgejo_api::Auth::OAuth2(&token.access_token), url.clone()).unwrap(); - let myself = token_api.user_get_current().await.unwrap(); - assert_eq!(myself.login.as_deref(), Some("TestingAdmin")); - - let request = forgejo_api::structs::OAuthTokenRequest::Refresh { - refresh_token: &token.refresh_token, - client_id: &client_id, - client_secret: &client_secret, - }; - let token = token_api.oauth_get_access_token(request).await.unwrap(); - let token_api = Forgejo::new(forgejo_api::Auth::OAuth2(&token.access_token), url).unwrap(); - let myself = token_api.user_get_current().await.unwrap(); - assert_eq!(myself.login.as_deref(), Some("TestingAdmin")); -} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..63ae02c --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,17 @@ +use forgejo_api::Forgejo; + +pub fn login() -> Forgejo { + let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap(); + let token = std::env::var("FORGEJO_API_CI_TOKEN").unwrap(); + Forgejo::new(forgejo_api::Auth::Token(&token), url).unwrap() +} + +pub fn login_pass(username: &str, password: &str) -> Forgejo { + let url = url::Url::parse(&std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap()).unwrap(); + let auth = forgejo_api::Auth::Password { + username, + password, + mfa: None, + }; + Forgejo::new(auth, url).unwrap() +} diff --git a/tests/repo.rs b/tests/repo.rs new file mode 100644 index 0000000..004cb8e --- /dev/null +++ b/tests/repo.rs @@ -0,0 +1,276 @@ +use forgejo_api::structs::*; + +mod common; + +struct Git { + dir: &'static std::path::Path, +} + +impl Git { + fn new<T: AsRef<std::path::Path> + ?Sized>(path: &'static T) -> Self { + let dir = path.as_ref(); + std::fs::create_dir_all(dir).unwrap(); + Self { dir } + } + + fn run(&self, args: &[impl AsRef<std::ffi::OsStr>]) { + let mut cmd = std::process::Command::new("git"); + cmd.current_dir(self.dir); + let _ = cmd.args(args).status().unwrap(); + } +} + +async fn basic_repo(api: &forgejo_api::Forgejo, git: &Git, name: &str) -> Repository { + git.run(&["config", "--global", "init.defaultBranch", "main"]); + git.run(&["init"]); + git.run(&["config", "user.name", "TestingAdmin"]); + git.run(&["config", "user.email", "admin@noreply.example.org"]); + tokio::fs::write(&git.dir.join("README.md"), "# Test\nThis is a test repo") + .await + .unwrap(); + git.run(&["add", "."]); + git.run(&["commit", "-m", "initial commit"]); + + let repo_opt = CreateRepoOption { + auto_init: Some(false), + default_branch: Some("main".into()), + description: Some("Test Repo".into()), + gitignores: Some("".into()), + issue_labels: Some("".into()), + license: Some("".into()), + name: name.into(), + object_format_name: None, + private: Some(false), + readme: None, + template: Some(false), + trust_model: Some(CreateRepoOptionTrustModel::Default), + }; + let remote_repo = api.create_current_user_repo(repo_opt).await.unwrap(); + assert!( + remote_repo.has_pull_requests.unwrap(), + "repo does not accept pull requests" + ); + assert!( + remote_repo.owner.as_ref().unwrap().login.as_ref().unwrap() == "TestingAdmin", + "repo owner is not \"TestingAdmin\"" + ); + assert!( + remote_repo.name.as_ref().unwrap() == name, + "repo name is not \"{name}\"" + ); + + let mut remote_url = remote_repo.clone_url.clone().unwrap(); + remote_url.set_username("TestingAdmin").unwrap(); + remote_url.set_password(Some("password")).unwrap(); + git.run(&["remote", "add", "origin", remote_url.as_str()]); + git.run(&["push", "-u", "origin", "main"]); + + remote_repo +} + +#[tokio::test] +async fn pull_request() { + let api = common::login(); + + let git = Git::new("./test_repos/pr"); + let _ = basic_repo(&api, &git, "pr-test").await; + git.run(&["switch", "-c", "test"]); + + tokio::fs::write( + "./test_repos/pr/example.rs", + "fn add_one(x: u32) -> u32 { x + 1 }", + ) + .await + .unwrap(); + git.run(&["add", "."]); + git.run(&["commit", "-m", "egg"]); + git.run(&["push", "-u", "origin", "test"]); + + let pr_opt = CreatePullRequestOption { + assignee: None, + assignees: Some(vec!["TestingAdmin".into()]), + base: Some("main".into()), + body: Some("This is a test PR".into()), + due_date: None, + head: Some("test".into()), + labels: None, + milestone: None, + title: Some("test pr".into()), + }; + + // Wait for... something to happen, or else creating a PR will return 404 + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + let pr = api + .repo_create_pull_request("TestingAdmin", "pr-test", pr_opt) + .await + .expect("couldn't create pr"); + + let is_merged = api + .repo_pull_request_is_merged("TestingAdmin", "pr-test", pr.number.unwrap()) + .await + .is_ok(); + assert!(!is_merged, "pr should not yet be merged"); + + let pr_files_query = RepoGetPullRequestFilesQuery::default(); + let (_, _) = api + .repo_get_pull_request_files( + "TestingAdmin", + "pr-test", + pr.number.unwrap(), + pr_files_query, + ) + .await + .unwrap(); + + let merge_opt = MergePullRequestOption { + r#do: MergePullRequestOptionDo::Merge, + merge_commit_id: None, + merge_message_field: None, + merge_title_field: None, + delete_branch_after_merge: Some(true), + force_merge: None, + head_commit_id: None, + merge_when_checks_succeed: None, + }; + + api.repo_merge_pull_request("TestingAdmin", "pr-test", pr.number.unwrap(), merge_opt) + .await + .expect("couldn't merge pr"); + let is_merged = api + .repo_pull_request_is_merged("TestingAdmin", "pr-test", pr.number.unwrap()) + .await + .is_ok(); + assert!(is_merged, "pr should be merged"); +} + +#[tokio::test] +async fn release() { + let api = common::login(); + + let git = Git::new("./test_repos/release"); + let _ = basic_repo(&api, &git, "release-test").await; + + // Wait for the repo to be finished being populated, or else creating a + // release will return "422 repo is empty" + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + + let query = RepoListReleasesQuery::default(); + assert!( + api.repo_list_releases("TestingAdmin", "release-test", query) + .await + .unwrap() + .is_empty(), + "there should be no releases yet" + ); + + let tag_opt = CreateTagOption { + message: Some("This is a tag!".into()), + tag_name: "v1.0".into(), + target: None, + }; + api.repo_create_tag("TestingAdmin", "release-test", tag_opt) + .await + .expect("failed to create tag"); + + let release_opt = CreateReleaseOption { + body: Some("This is a release!".into()), + draft: Some(true), + name: Some("v1.0".into()), + prerelease: Some(false), + tag_name: "v1.0".into(), + target_commitish: None, + }; + let release = api + .repo_create_release("TestingAdmin", "release-test", release_opt) + .await + .expect("failed to create release"); + let edit_release = EditReleaseOption { + body: None, + draft: Some(false), + name: None, + prerelease: None, + tag_name: None, + target_commitish: None, + }; + api.repo_edit_release( + "TestingAdmin", + "release-test", + release.id.unwrap(), + edit_release, + ) + .await + .expect("failed to edit release"); + + let release_by_tag = api + .repo_get_release_by_tag("TestingAdmin", "release-test", "v1.0") + .await + .expect("failed to find release"); + let release_latest = api + .repo_get_latest_release("TestingAdmin", "release-test") + .await + .expect("failed to find latest release"); + assert!(release_by_tag == release_latest, "releases not equal"); + + let attachment = api + .repo_create_release_attachment( + "TestingAdmin", + "release-test", + release.id.unwrap(), + b"This is a file!".to_vec(), + RepoCreateReleaseAttachmentQuery { + name: Some("test.txt".into()), + }, + ) + .await + .expect("failed to create release attachment"); + assert!( + &*api + .download_release_attachment( + "TestingAdmin", + "release-test", + release.id.unwrap(), + attachment.id.unwrap() + ) + .await + .unwrap() + == b"This is a file!", + "couldn't download attachment" + ); + let _zip_archive = api + .repo_get_archive("TestingAdmin", "release-test", "v1.0.zip") + .await + .unwrap(); + let _tar_archive = api + .repo_get_archive("TestingAdmin", "release-test", "v1.0.tar.gz") + .await + .unwrap(); + // check these contents when their return value is fixed + + api.repo_delete_release_attachment( + "TestingAdmin", + "release-test", + release.id.unwrap(), + attachment.id.unwrap(), + ) + .await + .expect("failed to deleted attachment"); + + api.repo_delete_release("TestingAdmin", "release-test", release.id.unwrap()) + .await + .expect("failed to delete release"); + + api.repo_delete_tag("TestingAdmin", "release-test", "v1.0") + .await + .expect("failed to delete release"); +} + +#[tokio::test] +async fn delete_repo() { + let api = common::login(); + let git = Git::new("./test_repos/delete"); + let _ = basic_repo(&api, &git, "delete-test").await; + + api.repo_delete("TestingAdmin", "delete-test") + .await + .expect("failed to delete repo"); +} diff --git a/tests/user.rs b/tests/user.rs new file mode 100644 index 0000000..9ca808d --- /dev/null +++ b/tests/user.rs @@ -0,0 +1,193 @@ +use forgejo_api::structs::*; + +mod common; + +#[tokio::test] +async fn myself() { + let api = common::login(); + + let myself = api.user_get_current().await.unwrap(); + assert!(myself.is_admin.unwrap(), "user should be admin"); + assert_eq!( + myself.login.as_ref().unwrap(), + "TestingAdmin", + "user should be named \"TestingAdmin\"" + ); + + let myself_indirect = api.user_get("TestingAdmin").await.unwrap(); + assert_eq!( + myself, myself_indirect, + "result of `myself` does not match result of `get_user`" + ); +} + +#[tokio::test] +async fn follow() { + let api = common::login(); + + let query = UserListFollowingQuery::default(); + let following = api + .user_list_following("TestingAdmin", query) + .await + .unwrap(); + assert!(following.is_empty(), "following list not empty"); + + let query = UserListFollowersQuery::default(); + let followers = api + .user_list_followers("TestingAdmin", query) + .await + .unwrap(); + assert!(followers.is_empty(), "follower list not empty"); + + let option = CreateUserOption { + created_at: None, + email: "follower@no-reply.example.org".into(), + full_name: None, + login_name: None, + must_change_password: Some(false), + password: Some("password".into()), + restricted: None, + send_notify: None, + source_id: None, + username: "Follower".into(), + visibility: None, + }; + let _ = api.admin_create_user(option).await.unwrap(); + let new_user = common::login_pass("Follower", "password"); + + new_user + .user_current_put_follow("TestingAdmin") + .await + .unwrap(); + api.user_current_put_follow("Follower").await.unwrap(); + + let query = UserListFollowingQuery::default(); + let following = api + .user_list_following("TestingAdmin", query) + .await + .unwrap(); + assert!(!following.is_empty(), "following list empty"); + + let query = UserListFollowersQuery::default(); + let followers = api + .user_list_followers("TestingAdmin", query) + .await + .unwrap(); + assert!(!followers.is_empty(), "follower list empty"); +} + +#[tokio::test] +async fn password_login() { + let api = common::login(); + let password_api = common::login_pass("TestingAdmin", "password"); + + assert!( + api.user_get_current().await.unwrap() == password_api.user_get_current().await.unwrap(), + "users not equal comparing token-auth and pass-auth" + ); +} + +#[tokio::test] +async fn oauth2_login() { + let api = common::login(); + let opt = forgejo_api::structs::CreateOAuth2ApplicationOptions { + confidential_client: Some(true), + name: Some("Test Application".into()), + redirect_uris: Some(vec!["http://127.0.0.1:48879/".into()]), + }; + let app = api.user_create_oauth2_application(opt).await.unwrap(); + let client_id = app.client_id.unwrap(); + let client_secret = app.client_secret.unwrap(); + + let base_url = &std::env::var("FORGEJO_API_CI_INSTANCE_URL").unwrap(); + + let client = reqwest::Client::builder() + .cookie_store(true) + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap(); + + // Log in via the web interface + let _ = client + .post(&format!("{base_url}user/login")) + .form(&[("user_name", "TestingAdmin"), ("password", "password")]) + .send() + .await + .unwrap() + .error_for_status() + .unwrap(); + + // Load the authorization page + let response = client + .get(&format!( + "{base_url}login/oauth/authorize\ + ?client_id={client_id}\ + &redirect_uri=http%3A%2F%2F127.0.0.1%3A48879%2F\ + &response_type=code\ + &state=theyve" + )) + .send() + .await + .unwrap() + .error_for_status() + .unwrap(); + let csrf = response.cookies().find(|x| x.name() == "_csrf").unwrap(); + + // Authorize the new application via the web interface + let response = client + .post(&format!("{base_url}login/oauth/grant")) + .form(&[ + ("_csrf", csrf.value()), + ("client_id", &client_id), + ("state", "theyve"), + ("scope", ""), + ("nonce", ""), + ("redirect_uri", "http://127.0.0.1:48879/"), + ]) + .send() + .await + .unwrap() + .error_for_status() + .unwrap(); + + // Extract the code from the redirect url + let location = response.headers().get(reqwest::header::LOCATION).unwrap(); + let location = url::Url::parse(dbg!(location.to_str().unwrap())).unwrap(); + let mut code = None; + for (key, value) in location.query_pairs() { + if key == "code" { + code = Some(value.into_owned()); + } else if key == "error_description" { + panic!("{value}"); + } + } + let code = code.unwrap(); + + // Redeem the code and check it works + let url = url::Url::parse(&base_url).unwrap(); + let api = forgejo_api::Forgejo::new(forgejo_api::Auth::None, url.clone()).unwrap(); + + let request = forgejo_api::structs::OAuthTokenRequest::Confidential { + client_id: &client_id, + client_secret: &client_secret, + code: &code, + redirect_uri: url::Url::parse("http://127.0.0.1:48879/").unwrap(), + }; + let token = api.oauth_get_access_token(request).await.unwrap(); + let token_api = + forgejo_api::Forgejo::new(forgejo_api::Auth::OAuth2(&token.access_token), url.clone()) + .unwrap(); + let myself = token_api.user_get_current().await.unwrap(); + assert_eq!(myself.login.as_deref(), Some("TestingAdmin")); + + let request = forgejo_api::structs::OAuthTokenRequest::Refresh { + refresh_token: &token.refresh_token, + client_id: &client_id, + client_secret: &client_secret, + }; + let token = token_api.oauth_get_access_token(request).await.unwrap(); + let token_api = + forgejo_api::Forgejo::new(forgejo_api::Auth::OAuth2(&token.access_token), url).unwrap(); + let myself = token_api.user_get_current().await.unwrap(); + assert_eq!(myself.login.as_deref(), Some("TestingAdmin")); +} |