Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-httpclient

Named HTTP clients for Pudu: a client factory, delegating handlers, pooled keep-alive connections, handler lifetimes, logging, and resilience

0.1.1Apache-2.01

InstallClose

PlaceholderContractTest.pudu

Pudu118 lines7.3 KB

GitHub ↗
1/** @Test.Integration.PlaceholderContract.Suite — the placeholder service read through a typed client */2module Integration.PlaceholderContractTest34import Std.Io as Io5import Std.Json as Json6import Std.Result as Result7import Std.Test as Test8import PuduLangHttpClient.Client as Client9import PuduLangHttpClient.Events as Events10import PuduLangHttpClient.Factory.Builder as Builder11import PuduLangHttpClient.Factory as Factory12import PuduLangHttpClient as HttpClient13import PuduLangHttpClient.Response as Response14import PuduLangHttpClient.Transport as Transport15import PuduLangHttpClient.Utils.Shared as Shared16import Support.Placeholder.Api as Api17import Support.Placeholder.Server as Placeholder1819/// The number of records an outcome holds, or -1 for a failure.20fn sizeOf[T](outcome: &HttpClient.Outcome[Array[T]]) -> Int {21  match outcome {22    case Ok(items) => items.length()23    case Err(_) => -124  }25}2627/// The ids of the records an outcome holds.28fn idsOf(outcome: &HttpClient.Outcome[Array[Api.Post]]) -> Array[Int] {29  match outcome {30    case Ok(items) => items.map(|post: Api.Post| post.id)31    case Err(_) => []32  }33}3435/// Whether every record of an outcome belongs to the owner.36fn allOwnedBy(outcome: &HttpClient.Outcome[Array[Api.Comment]], postId: Int) -> Bool {37  match outcome {38    case Ok(items) => items.filter(|comment: Api.Comment| comment.postId != postId).isEmpty() && !items.isEmpty()39    case Err(_) => false40  }41}4243/// Runs the suite.44fn main() -> Int {45  let service = Placeholder.start()46  let completed = Shared.shared(0)47  let listeners = Events.listeners().onRequestCompleted(|_event: Events.RequestCompleted| Shared.update(&completed, |count: Int| count + 1))48  let factory = match Factory.buildWith(&Factory.Options{listeners: listeners}, [49      Builder.defaults().withDefaultHeader("user-agent", "placeholder-suite/1.0").withTimeout(10000),50      Builder.named("placeholder").withBaseAddress(service.base + "/").withTransport(Transport.defaults().withMaxConnectionsPerServer(8).withDecompression())51    ]) {52    case Ok(built) => built53    case Err(invalid) => panic(Factory.explain(&invalid))54  }55  let api = Factory.typed(&factory, "placeholder", |client: Client.Client| Api.Api{client: client})5657  let posts: HttpClient.Outcome[Array[Api.Post]] = Api.list(&api, "posts", &[])58  let comments: HttpClient.Outcome[Array[Api.Comment]] = Api.list(&api, "comments", &[])59  let albums: HttpClient.Outcome[Array[Api.Album]] = Api.list(&api, "albums", &[])60  let photos: HttpClient.Outcome[Array[Api.Photo]] = Api.list(&api, "photos", &[])61  let todos: HttpClient.Outcome[Array[Api.Todo]] = Api.list(&api, "todos", &[])62  let users: HttpClient.Outcome[Array[Api.User]] = Api.list(&api, "users", &[])6364  let firstUser: HttpClient.Outcome[Api.User] = Api.find(&api, "users", 1)65  let post: HttpClient.Outcome[Api.Post] = Api.find(&api, "posts", 42)66  let photo: HttpClient.Outcome[Api.Photo] = Api.find(&api, "photos", 5000)67  let missing: HttpClient.Outcome[Api.Post] = Api.find(&api, "posts", 101)68  let notANumber = Result.map(Client.get(&api.client, "posts/abc"), |r: Response.Response| r.status.code)69  let unknownResource = Result.map(Client.get(&api.client, "widgets"), |r: Response.Response| r.status.code)7071  let nestedComments: HttpClient.Outcome[Array[Api.Comment]] = Api.children(&api, "posts", 7, "comments")72  let filteredComments: HttpClient.Outcome[Array[Api.Comment]] = Api.list(&api, "comments", &[("postId", "7")])73  let userPosts: HttpClient.Outcome[Array[Api.Post]] = Api.children(&api, "users", 3, "posts")74  let albumPhotos: HttpClient.Outcome[Array[Api.Photo]] = Api.children(&api, "albums", 2, "photos")75  let userAlbums: HttpClient.Outcome[Array[Api.Album]] = Api.children(&api, "users", 10, "albums")76  let userTodos: HttpClient.Outcome[Array[Api.Todo]] = Api.children(&api, "users", 4, "todos")77  let orphan: HttpClient.Outcome[Array[Api.Post]] = Api.children(&api, "users", 11, "posts")78  let doneTodos: HttpClient.Outcome[Array[Api.Todo]] = Api.list(&api, "todos", &[("userId", "1"), ("completed", "true")])7980  let page: HttpClient.Outcome[Api.Page[Api.Post]] = Api.page(&api, "posts", &[("_start", "10"), ("_limit", "5")])81  let sorted: HttpClient.Outcome[Array[Api.Post]] = Api.list(&api, "posts", &[("_sort", "id"), ("_order", "desc"), ("_limit", "3")])82  let pastEnd: HttpClient.Outcome[Array[Api.Post]] = Api.list(&api, "posts", &[("_start", "500")])83  let headers = Result.map(Client.get(&api.client, "users/1"), |r: Response.Response| Response.header(&r, "content-type"))8485  let stats = Factory.statistics(&factory, "placeholder")86  let sent = Placeholder.handled(&service)87  Factory.dispose(&factory)88  Placeholder.stop(&service)8990  let checks = Test.suite("Integration.PlaceholderContract", &[91      Test.equals("every resource holds the public number of records", &[sizeOf(&posts), sizeOf(&comments), sizeOf(&albums), sizeOf(&photos), sizeOf(&todos), sizeOf(&users)], &[100, 500, 100, 5000, 200, 10]),92      Test.equals("a user decodes with its nested address, position, and company", &Result.map(firstUser, |user: Api.User| (user.username, user.address.city, user.address.geo.lat, user.company.name)), &Ok(("Bret", "Gwenborough", "-37.3159", "Romaguera-Crona"))),93      Test.equals("a post belongs to its owner by tens", &Result.map(post, |found: Api.Post| (found.id, found.userId, found.title)), &Ok((42, 5, "post 42"))),94      Test.equals("the last photo belongs to the last album", &Result.map(photo, |found: Api.Photo| (found.albumId, found.thumbnailUrl)), &Ok((100, "https://picsum.photos/seed/5000/150"))),95      Test.equals("a missing record is not found", &missing, &Err(HttpClient.Unsuccessful(404, "Not Found"))),96      Test.equals("an id that is not a number and an unknown resource are not found", &(notANumber, unknownResource), &(Ok(404), Ok(404))),97      Test.that("a post's nested comments are its five comments", sizeOf(&nestedComments) == 5 && allOwnedBy(&nestedComments, 7)),98      Test.equals("a nested listing equals the filtered one", &nestedComments, &filteredComments),99      Test.equals("nested listings hold each owner's share", &[sizeOf(&userPosts), sizeOf(&albumPhotos), sizeOf(&userAlbums), sizeOf(&userTodos)], &[10, 50, 10, 20]),100      Test.equals("a missing parent is not found", &Result.map(orphan, |items: Array[Api.Post]| items.length()), &Err(HttpClient.Unsuccessful(404, "Not Found"))),101      Test.equals("filters combine", &Result.map(doneTodos, |items: Array[Api.Todo]| items.map(|todo: Api.Todo| todo.id)), &Ok([3, 6, 9, 12, 15, 18])),102      Test.equals("a page starts and stops where asked and reports the total", &Result.map(page, |found: Api.Page[Api.Post]| (found.items.map(|p: Api.Post| p.id), found.total)), &Ok(([11, 12, 13, 14, 15], 100))),103      Test.equals("a listing sorts in descending order", &idsOf(&sorted), &[100, 99, 98]),104      Test.equals("a start past the end lists nothing", &sizeOf(&pastEnd), &0),105      Test.equals("responses are JSON", &headers, &Ok(Some("application/json"))),106      Test.that("one pool served every request over a few kept-alive connections", match stats {107          case Some(found) => found.opened <= 2 && found.reused >= 20108          case None => false109        }),110      Test.equals("every request was heard once, and each but the unrouted one reached a route once", &(Shared.current(&completed), sent), &(24, 23))111    ])112  let ran = Test.run(&checks)113  for failure in Test.failuresOf(&ran) {114    let _reported = Io.writeErrorLine(failure)115  }116  Test.report(&ran)117}118