use futures::future::BoxFuture;
use futures::StreamExt;
use serde_json::{json, Value};
use wiremock::matchers::any;
use wiremock::{Mock, MockServer, ResponseTemplate};
use crate::{EventStream, MisarDev, MisarDevError};
type Call = for<'a> fn(&'a MisarDev) -> BoxFuture<'a, Result<Value, MisarDevError>>;
type StreamCall = for<'a> fn(&'a MisarDev) -> BoxFuture<'a, Result<EventStream, MisarDevError>>;
pub(crate) const OPERATIONS: &[(&str, &str, &str, bool, Call)] = &[
("account.createKey", "POST", "/account/api-keys", true, |d| Box::pin(d.account().create_key(Some(json!({"probe": true}))))),
("account.deleteKey", "DELETE", "/account/api-keys/keyId-1", false, |d| Box::pin(d.account().delete_key("keyId-1"))),
("account.export", "GET", "/account/export", false, |d| Box::pin(d.account().export())),
("account.listKeys", "GET", "/account/api-keys", false, |d| Box::pin(d.account().list_keys())),
("account.updateKey", "PATCH", "/account/api-keys/keyId-1", true, |d| Box::pin(d.account().update_key("keyId-1", Some(json!({"probe": true}))))),
("apiKeys.create", "POST", "/api-keys", true, |d| Box::pin(d.api_keys().create(Some(json!({"probe": true}))))),
("apiKeys.delete", "DELETE", "/api-keys", true, |d| Box::pin(d.api_keys().delete(Some(json!({"probe": true}))))),
("apiKeys.list", "GET", "/api-keys", false, |d| Box::pin(d.api_keys().list(None))),
("audit.list", "GET", "/audit", false, |d| Box::pin(d.audit().list())),
("billing.checkout", "POST", "/billing/checkout", true, |d| Box::pin(d.billing().checkout(Some(json!({"probe": true}))))),
("billing.comparison", "GET", "/billing/comparison", false, |d| Box::pin(d.billing().comparison())),
("billing.plans", "GET", "/billing/plans", false, |d| Box::pin(d.billing().plans())),
("billing.subscription", "GET", "/billing/subscription", false, |d| Box::pin(d.billing().subscription())),
("billing.switchPlan", "POST", "/billing/switch-plan", true, |d| Box::pin(d.billing().switch_plan(Some(json!({"probe": true}))))),
("builds.list", "GET", "/builds", false, |d| Box::pin(d.builds().list(None))),
("chat.completions", "POST", "/v1/chat/completions", true, |d| Box::pin(d.chat().completions(Some(json!({"probe": true}))))),
("checkpoints.delete", "DELETE", "/checkpoints/id-1", false, |d| Box::pin(d.checkpoints().delete("id-1"))),
("checkpoints.restore", "POST", "/checkpoints/id-1/restore", false, |d| Box::pin(d.checkpoints().restore("id-1"))),
("checkpoints.update", "PATCH", "/checkpoints/id-1", true, |d| Box::pin(d.checkpoints().update("id-1", Some(json!({"probe": true}))))),
("connectors.available", "GET", "/connectors/available", false, |d| Box::pin(d.connectors().available())),
("connectors.createConnection", "POST", "/connectors/connections", true, |d| Box::pin(d.connectors().create_connection(Some(json!({"probe": true}))))),
("connectors.deleteConnection", "DELETE", "/connectors/connections/id-1", false, |d| Box::pin(d.connectors().delete_connection("id-1"))),
("connectors.list", "GET", "/connectors", false, |d| Box::pin(d.connectors().list())),
("connectors.listConnections", "GET", "/connectors/connections", false, |d| Box::pin(d.connectors().list_connections(None))),
("connectors.testConnection", "POST", "/connectors/connections/id-1/test", false, |d| Box::pin(d.connectors().test_connection("id-1"))),
("connectors.toggle", "POST", "/connectors/slug-1/toggle", true, |d| Box::pin(d.connectors().toggle("slug-1", Some(json!({"probe": true}))))),
("connectors.updateConnection", "PATCH", "/connectors/connections/id-1", true, |d| Box::pin(d.connectors().update_connection("id-1", Some(json!({"probe": true}))))),
("domains.lookup", "GET", "/domains/lookup?domain=domain-v", false, |d| Box::pin(d.domains().lookup("domain-v"))),
("gallery.like", "POST", "/gallery/projectId-1/like", false, |d| Box::pin(d.gallery().like("projectId-1"))),
("gallery.list", "GET", "/gallery", false, |d| Box::pin(d.gallery().list(None))),
("gallery.unlike", "DELETE", "/gallery/projectId-1/like", false, |d| Box::pin(d.gallery().unlike("projectId-1"))),
("github.disconnect", "DELETE", "/github/connection", false, |d| Box::pin(d.github().disconnect())),
("github.getConnection", "GET", "/github/connection", false, |d| Box::pin(d.github().get_connection())),
("notifications.list", "GET", "/notifications", false, |d| Box::pin(d.notifications().list(None))),
("notifications.markAllRead", "PATCH", "/notifications", true, |d| Box::pin(d.notifications().mark_all_read(Some(json!({"probe": true}))))),
("notifications.markRead", "PATCH", "/notifications/id-1", true, |d| Box::pin(d.notifications().mark_read("id-1", Some(json!({"probe": true}))))),
("organizations.acceptInvitation", "POST", "/invitations/accept", true, |d| Box::pin(d.organizations().accept_invitation(Some(json!({"probe": true}))))),
("organizations.create", "POST", "/organizations", true, |d| Box::pin(d.organizations().create(Some(json!({"probe": true}))))),
("organizations.createInvitation", "POST", "/organizations/id-1/invitations", true, |d| Box::pin(d.organizations().create_invitation("id-1", Some(json!({"probe": true}))))),
("organizations.list", "GET", "/organizations", false, |d| Box::pin(d.organizations().list())),
("organizations.listInvitations", "GET", "/organizations/id-1/invitations", false, |d| Box::pin(d.organizations().list_invitations("id-1"))),
("organizations.listMembers", "GET", "/organizations/id-1/members", false, |d| Box::pin(d.organizations().list_members("id-1"))),
("organizations.removeMember", "DELETE", "/organizations/id-1/members?userId=userId-v", false, |d| Box::pin(d.organizations().remove_member("id-1", "userId-v"))),
("organizations.revokeInvitation", "DELETE", "/organizations/id-1/invitations?invitationId=invitationId-v", false, |d| Box::pin(d.organizations().revoke_invitation("id-1", "invitationId-v"))),
("organizations.updateMember", "PATCH", "/organizations/id-1/members", true, |d| Box::pin(d.organizations().update_member("id-1", Some(json!({"probe": true}))))),
("profile.follow", "POST", "/follow/handle-1", false, |d| Box::pin(d.profile().follow("handle-1"))),
("profile.get", "GET", "/profile/handle-1", false, |d| Box::pin(d.profile().get("handle-1"))),
("profile.unfollow", "DELETE", "/follow/handle-1", false, |d| Box::pin(d.profile().unfollow("handle-1"))),
("projects.addCollaborator", "POST", "/projects/id-1/collaborators", true, |d| Box::pin(d.projects().add_collaborator("id-1", Some(json!({"probe": true}))))),
("projects.archive", "POST", "/projects/id-1/archive", true, |d| Box::pin(d.projects().archive("id-1", Some(json!({"probe": true}))))),
("projects.buildStatus", "GET", "/projects/id-1/build-status", false, |d| Box::pin(d.projects().build_status("id-1"))),
("projects.cancelBuild", "POST", "/projects/id-1/cancel-build", false, |d| Box::pin(d.projects().cancel_build("id-1"))),
("projects.checkSubdomain", "GET", "/projects/id-1/subdomain/check?value=value-v", false, |d| Box::pin(d.projects().check_subdomain("id-1", "value-v"))),
("projects.create", "POST", "/projects", true, |d| Box::pin(d.projects().create(Some(json!({"probe": true}))))),
("projects.createConversation", "POST", "/projects/id-1/conversations", false, |d| Box::pin(d.projects().create_conversation("id-1"))),
("projects.delete", "DELETE", "/projects/id-1", false, |d| Box::pin(d.projects().delete("id-1"))),
("projects.deleteEnv", "DELETE", "/projects/id-1/env?key=key-v", false, |d| Box::pin(d.projects().delete_env("id-1", "key-v"))),
("projects.deleteSkills", "DELETE", "/projects/id-1/skills?slug=slug-v", false, |d| Box::pin(d.projects().delete_skills("id-1", "slug-v"))),
("projects.deploy", "POST", "/projects/id-1/deploy", true, |d| Box::pin(d.projects().deploy("id-1", Some(json!({"probe": true}))))),
("projects.duplicate", "POST", "/projects/id-1/duplicate", false, |d| Box::pin(d.projects().duplicate("id-1"))),
("projects.export", "GET", "/projects/id-1/export", false, |d| Box::pin(d.projects().export("id-1"))),
("projects.get", "GET", "/projects/id-1", false, |d| Box::pin(d.projects().get("id-1"))),
("projects.getBackend", "GET", "/projects/id-1/backend", false, |d| Box::pin(d.projects().get_backend("id-1"))),
("projects.getDeployment", "GET", "/projects/id-1/deploy", false, |d| Box::pin(d.projects().get_deployment("id-1"))),
("projects.getDomain", "GET", "/projects/id-1/domain", false, |d| Box::pin(d.projects().get_domain("id-1"))),
("projects.getEnv", "GET", "/projects/id-1/env", false, |d| Box::pin(d.projects().get_env("id-1"))),
("projects.getKnowledge", "GET", "/projects/id-1/knowledge", false, |d| Box::pin(d.projects().get_knowledge("id-1"))),
("projects.getSkills", "GET", "/projects/id-1/skills", false, |d| Box::pin(d.projects().get_skills("id-1"))),
("projects.getTools", "GET", "/projects/id-1/tools", false, |d| Box::pin(d.projects().get_tools("id-1"))),
("projects.importArtifact", "POST", "/projects/id-1/import-artifact", true, |d| Box::pin(d.projects().import_artifact("id-1", Some(json!({"probe": true}))))),
("projects.list", "GET", "/projects", false, |d| Box::pin(d.projects().list(None))),
("projects.listCheckpoints", "GET", "/projects/id-1/checkpoints", false, |d| Box::pin(d.projects().list_checkpoints("id-1"))),
("projects.listCollaborators", "GET", "/projects/id-1/collaborators", false, |d| Box::pin(d.projects().list_collaborators("id-1"))),
("projects.listConversations", "GET", "/projects/id-1/conversations", false, |d| Box::pin(d.projects().list_conversations("id-1", None))),
("projects.listFiles", "GET", "/projects/id-1/files", false, |d| Box::pin(d.projects().list_files("id-1"))),
("projects.provisionBackend", "POST", "/projects/id-1/backend", true, |d| Box::pin(d.projects().provision_backend("id-1", Some(json!({"probe": true}))))),
("projects.removeCollaborator", "DELETE", "/projects/id-1/collaborators?collaboratorId=collaboratorId-v", false, |d| Box::pin(d.projects().remove_collaborator("id-1", "collaboratorId-v"))),
("projects.removeDomain", "DELETE", "/projects/id-1/domain", false, |d| Box::pin(d.projects().remove_domain("id-1"))),
("projects.restore", "POST", "/projects/id-1", false, |d| Box::pin(d.projects().restore("id-1"))),
("projects.setDomain", "POST", "/projects/id-1/domain", true, |d| Box::pin(d.projects().set_domain("id-1", Some(json!({"probe": true}))))),
("projects.undeploy", "DELETE", "/projects/id-1/deploy", false, |d| Box::pin(d.projects().undeploy("id-1"))),
("projects.update", "PATCH", "/projects/id-1", true, |d| Box::pin(d.projects().update("id-1", Some(json!({"probe": true}))))),
("projects.updateEnv", "PUT", "/projects/id-1/env", true, |d| Box::pin(d.projects().update_env("id-1", Some(json!({"probe": true}))))),
("projects.updateKnowledge", "PUT", "/projects/id-1/knowledge", true, |d| Box::pin(d.projects().update_knowledge("id-1", Some(json!({"probe": true}))))),
("projects.updateSkills", "PUT", "/projects/id-1/skills", true, |d| Box::pin(d.projects().update_skills("id-1", Some(json!({"probe": true}))))),
("projects.updateSubdomain", "PATCH", "/projects/id-1/subdomain", true, |d| Box::pin(d.projects().update_subdomain("id-1", Some(json!({"probe": true}))))),
("projects.updateTools", "PUT", "/projects/id-1/tools", true, |d| Box::pin(d.projects().update_tools("id-1", Some(json!({"probe": true}))))),
("projects.writeFiles", "POST", "/projects/id-1/files", true, |d| Box::pin(d.projects().write_files("id-1", Some(json!({"probe": true}))))),
("rag.clear", "DELETE", "/rag?projectId=projectId-v", false, |d| Box::pin(d.rag().clear("projectId-v"))),
("rag.index", "POST", "/rag", true, |d| Box::pin(d.rag().index(Some(json!({"probe": true}))))),
("rag.search", "GET", "/rag?projectId=projectId-v", false, |d| Box::pin(d.rag().search("projectId-v", None))),
("reports.create", "POST", "/reports", true, |d| Box::pin(d.reports().create(Some(json!({"probe": true}))))),
("search.code", "GET", "/search", false, |d| Box::pin(d.search().code(None))),
("search.web", "POST", "/v1/search", true, |d| Box::pin(d.search().web(Some(json!({"probe": true}))))),
("skills.get", "GET", "/skills/misarai/slug-1", false, |d| Box::pin(d.skills().get("slug-1"))),
("skills.list", "GET", "/skills/misarai", false, |d| Box::pin(d.skills().list())),
("templates.create", "POST", "/templates", true, |d| Box::pin(d.templates().create(Some(json!({"probe": true}))))),
("templates.delete", "DELETE", "/templates/id-1", false, |d| Box::pin(d.templates().delete("id-1"))),
("templates.get", "GET", "/templates/id-1", false, |d| Box::pin(d.templates().get("id-1"))),
("templates.list", "GET", "/templates", false, |d| Box::pin(d.templates().list(None))),
("templates.remix", "POST", "/templates/remix", true, |d| Box::pin(d.templates().remix(Some(json!({"probe": true}))))),
("templates.update", "PATCH", "/templates/id-1", true, |d| Box::pin(d.templates().update("id-1", Some(json!({"probe": true}))))),
("templates.use", "POST", "/templates/id-1/use", true, |d| Box::pin(d.templates().use_("id-1", Some(json!({"probe": true}))))),
("usage.list", "GET", "/usage", false, |d| Box::pin(d.usage().list(None))),
("usage.plan", "GET", "/usage/plan", false, |d| Box::pin(d.usage().plan())),
("usage.track", "POST", "/usage/track", true, |d| Box::pin(d.usage().track(Some(json!({"probe": true}))))),
];
pub(crate) const STREAMS: &[(&str, &str, &str, bool, StreamCall)] = &[
("chat.studioStream", "POST", "/chat", true, |d| Box::pin(d.chat().studio_stream(Some(json!({"probe": true}))))),
("chat.completionsStream", "POST", "/v1/chat/completions", true, |d| Box::pin(d.chat().completions_stream(Some(json!({"probe": true}))))),
("projects.buildStream", "GET", "/projects/id-1/build-stream", false, |d| Box::pin(d.projects().build_stream("id-1"))),
];
fn client(server: &MockServer) -> MisarDev {
MisarDev::builder()
.api_key("mdk_test_key")
.base_url(format!("{}/dev", server.uri()))
.build()
.expect("client")
}
fn url_of(req: &wiremock::Request) -> String {
match req.url.query() {
Some(q) => format!("{}?{}", req.url.path(), q),
None => req.url.path().to_owned(),
}
}
#[test]
fn table_covers_every_operation() {
assert_eq!(OPERATIONS.len() + STREAMS.len(), super::OPERATION_COUNT);
}
#[tokio::test]
async fn every_generated_method_builds_its_route() {
let server = MockServer::start().await;
for (name, verb, url, has_body, call) in OPERATIONS {
server.reset().await;
Mock::given(any())
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"ok": true})))
.mount(&server)
.await;
let dev = client(&server);
let out = call(&dev).await.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(out, json!({"ok": true}), "{name}");
let reqs = server.received_requests().await.expect("recording");
assert_eq!(reqs.len(), 1, "{name}");
assert_eq!(reqs[0].method.as_str(), *verb, "{name}");
assert_eq!(url_of(&reqs[0]), format!("/dev{url}"), "{name}");
if *has_body {
let body: Value = serde_json::from_slice(&reqs[0].body).expect("json body");
assert_eq!(body["probe"], json!(true), "{name}");
}
}
}
#[tokio::test]
async fn every_generated_stream_builds_its_route() {
let server = MockServer::start().await;
for (name, verb, url, has_body, call) in STREAMS {
server.reset().await;
Mock::given(any())
.respond_with(
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string("data: {\"x\":1}\n\n"),
)
.mount(&server)
.await;
let dev = client(&server);
let mut stream = call(&dev).await.unwrap_or_else(|e| panic!("{name}: {e}"));
let mut n = 0;
while let Some(ev) = stream.next().await {
assert_eq!(ev.expect("event").data, json!({"x": 1}), "{name}");
n += 1;
}
assert_eq!(n, 1, "{name}");
let reqs = server.received_requests().await.expect("recording");
assert_eq!(reqs[0].method.as_str(), *verb, "{name}");
assert_eq!(url_of(&reqs[0]), format!("/dev{url}"), "{name}");
if *has_body {
let body: Value = serde_json::from_slice(&reqs[0].body).expect("json body");
assert_eq!(body["probe"], json!(true), "{name}");
}
}
}