Tasks demo TestClient suite
Drives the Tasks app through the in-process TestClient.
- Path
gebweb/examples/tasks/main_test.gb- Category
- Tasks
Covers the login flow, owner-scoped CRUD, cross-user isolation,
admin role gating, and multipart file upload.
Source
import test;
import gebweb;
import json;
import tasksapp;
const BOUNDARY = "----tasks-test-boundary";
func bearer(string token): dict<string, any> {
return {"Authorization": "Bearer " + token};
}
func multipartFile(string fieldName, string filename, string contentType, string body): string {
return "------tasks-test-boundary\r\n"
+ "Content-Disposition: form-data; name=\"" + fieldName
+ "\"; filename=\"" + filename + "\"\r\n"
+ "Content-Type: " + contentType + "\r\n\r\n"
+ body + "\r\n"
+ "------tasks-test-boundary--\r\n";
}
func multipartHeaders(string token): dict<string, any> {
return {
"Authorization": "Bearer " + token,
"Content-Type": "multipart/form-data; boundary=----tasks-test-boundary",
};
}
class LoginTest extends test.Test {
gebweb.TestClient client;
func setUp(): void {
this.client = gebweb.TestClient(tasksapp.build());
}
@test
func loginAsKnownUserMintsToken(): void {
let r = this.client.post("/login", {"username": "ada"});
r.assertStatus(200);
this.assertTrue(!(r.json()["token"] as string).isEmpty());
this.assertEquals(3600, r.json()["expires_in"] as int);
}
@test
func loginAsUnknownUserIs401(): void {
let r = this.client.post("/login", {"username": "nobody"});
r.assertStatus(401);
}
@test
func tasksRequireAuth(): void {
let r = this.client.get("/tasks");
r.assertStatus(401);
}
@test
func invalidBearerIs401(): void {
let r = this.client.request("GET", "/tasks", null, bearer("garbage"));
r.assertStatus(401);
}
}
class TaskCrudTest extends test.Test {
gebweb.TestClient client;
string adaToken;
string carlaToken;
func setUp(): void {
this.client = gebweb.TestClient(tasksapp.build());
this.adaToken = (this.client.post("/login", {"username": "ada"})
.json()["token"]) as string;
this.carlaToken = (this.client.post("/login", {"username": "carla"})
.json()["token"]) as string;
}
@test
func createAndListOwnTasks(): void {
let a = this.client.request("POST", "/tasks",
{"title": "write docs"}, bearer(this.adaToken));
a.assertStatus(200);
let id = a.json()["id"] as string;
this.assertEquals("write docs", a.json()["title"] as string);
this.assertEquals(false, a.json()["done"]);
let list = this.client.request("GET", "/tasks", null, bearer(this.adaToken));
list.assertStatus(200);
this.assertEquals(1, list.json()["total"] as int);
let items = list.json()["items"] as list<any>;
this.assertEquals(id, (items[0] as tasksapp.Task).id);
}
@test
func anotherUserCannotSeeMyTask(): void {
let r = this.client.request("POST", "/tasks",
{"title": "ada's task"}, bearer(this.adaToken));
let id = r.json()["id"] as string;
/* Carla's list excludes Ada's task entirely. */
let carlaList = this.client.request("GET", "/tasks", null,
bearer(this.carlaToken));
this.assertEquals(0, carlaList.json()["total"] as int);
/* And cannot read Ada's task by id. */
let carlaRead = this.client.request("GET", "/tasks/" + id, null,
bearer(this.carlaToken));
carlaRead.assertStatus(404);
}
@test
func patchUpdatesFields(): void {
let created = this.client.request("POST", "/tasks",
{"title": "draft"}, bearer(this.adaToken));
let id = created.json()["id"] as string;
let patched = this.client.request("PATCH", "/tasks/" + id,
{"title": "final", "done": true}, bearer(this.adaToken));
patched.assertStatus(200);
this.assertEquals("final", patched.json()["title"] as string);
this.assertEquals(true, patched.json()["done"]);
}
@test
func deleteRemovesTask(): void {
let created = this.client.request("POST", "/tasks",
{"title": "doomed"}, bearer(this.adaToken));
let id = created.json()["id"] as string;
let deleted = this.client.request("DELETE", "/tasks/" + id, null,
bearer(this.adaToken));
deleted.assertStatus(204);
let read = this.client.request("GET", "/tasks/" + id, null,
bearer(this.adaToken));
read.assertStatus(404);
}
@test
func cannotDeleteAnotherUsersTask(): void {
let created = this.client.request("POST", "/tasks",
{"title": "ada's"}, bearer(this.adaToken));
let id = created.json()["id"] as string;
let carlaDelete = this.client.request("DELETE", "/tasks/" + id, null,
bearer(this.carlaToken));
carlaDelete.assertStatus(404);
}
}
class AdminTest extends test.Test {
gebweb.TestClient client;
string adaToken;
string carlaToken;
func setUp(): void {
this.client = gebweb.TestClient(tasksapp.build());
this.adaToken = (this.client.post("/login", {"username": "ada"})
.json()["token"]) as string;
this.carlaToken = (this.client.post("/login", {"username": "carla"})
.json()["token"]) as string;
/* Seed: one task from each user. */
this.client.request("POST", "/tasks", {"title": "ada-1"},
bearer(this.adaToken));
this.client.request("POST", "/tasks", {"title": "carla-1"},
bearer(this.carlaToken));
}
@test
func adminSeesAllTasksAcrossUsers(): void {
let r = this.client.request("GET", "/admin/tasks", null,
bearer(this.carlaToken));
r.assertStatus(200);
this.assertEquals(2, r.json()["total"] as int);
}
@test
func nonAdminGetsForbidden(): void {
let r = this.client.request("GET", "/admin/tasks", null,
bearer(this.adaToken));
r.assertStatus(403);
}
@test
func adminCanForceDeleteAnyTask(): void {
let list = this.client.request("GET", "/admin/tasks", null,
bearer(this.carlaToken));
let items = list.json()["items"] as list<any>;
let target = items[0] as tasksapp.Task;
let id = target.id;
let removed = this.client.request("DELETE", "/admin/tasks/" + id, null,
bearer(this.carlaToken));
removed.assertStatus(204);
let after = this.client.request("GET", "/admin/tasks", null,
bearer(this.carlaToken));
this.assertEquals(1, after.json()["total"] as int);
}
}
class AttachmentTest extends test.Test {
gebweb.TestClient client;
string adaToken;
string carlaToken;
string taskId;
func setUp(): void {
this.client = gebweb.TestClient(tasksapp.build());
this.adaToken = (this.client.post("/login", {"username": "ada"})
.json()["token"]) as string;
this.carlaToken = (this.client.post("/login", {"username": "carla"})
.json()["token"]) as string;
let created = this.client.request("POST", "/tasks",
{"title": "with attachment"}, bearer(this.adaToken));
this.taskId = created.json()["id"] as string;
}
@test
func uploadAttachmentToOwnTask(): void {
let body = multipartFile("file", "notes.md", "text/markdown",
"# Hello\nthis is a note");
let r = this.client.request("POST",
"/tasks/" + this.taskId + "/attachments", body,
multipartHeaders(this.adaToken));
r.assertStatus(200);
this.assertEquals("notes.md", r.json()["filename"] as string);
this.assertEquals(22, r.json()["size"] as int);
}
@test
func uploadListedAfterUpload(): void {
let body = multipartFile("file", "a.txt", "text/plain", "alpha");
this.client.request("POST",
"/tasks/" + this.taskId + "/attachments", body,
multipartHeaders(this.adaToken)).assertStatus(200);
let list = this.client.request("GET",
"/tasks/" + this.taskId + "/attachments", null,
bearer(this.adaToken));
list.assertStatus(200);
let items = list.json()["items"] as list<any>;
this.assertEquals(1, items.length());
this.assertEquals("a.txt",
(items[0] as dict<string, any>)["filename"] as string);
}
@test
func cannotUploadToAnotherUsersTask(): void {
let body = multipartFile("file", "x.txt", "text/plain", "x");
let r = this.client.request("POST",
"/tasks/" + this.taskId + "/attachments", body,
multipartHeaders(this.carlaToken));
r.assertStatus(404);
}
@test
func missingFilePartIs400(): void {
/* multipart body with no `file` part - empty boundary. */
let body = "------tasks-test-boundary\r\n"
+ "Content-Disposition: form-data; name=\"label\"\r\n\r\n"
+ "decoy\r\n"
+ "------tasks-test-boundary--\r\n";
let r = this.client.request("POST",
"/tasks/" + this.taskId + "/attachments", body,
multipartHeaders(this.adaToken));
r.assertStatus(400);
}
}
class OpenApiTest extends test.Test {
gebweb.TestClient client;
func setUp(): void {
this.client = gebweb.TestClient(tasksapp.build());
}
@test
func openapiSpecAdvertisesAllRoutes(): void {
let r = this.client.get("/openapi.json");
r.assertStatus(200);
let spec = json.parse(r.text()) as dict<string, any>;
let paths = spec["paths"] as dict<string, any>;
this.assertTrue(paths.contains("/login"));
this.assertTrue(paths.contains("/tasks"));
this.assertTrue(paths.contains("/tasks/{id}"));
this.assertTrue(paths.contains("/tasks/{id}/attachments"));
this.assertTrue(paths.contains("/admin/tasks"));
this.assertTrue(paths.contains("/admin/tasks/{id}"));
}
}