Feature-tour task manager
End-to-end TestClient drive of a broad framework
- Path
gebweb/examples/feature_tour/tests/tasks_test.gb- Category
- Feature Tour
surface: @ApiResource-style CRUD via api-key auth, cursor
pagination with query DSL, attachment upload to storage,
task.completed event -> background-job mailer, scheduled
cleanup of stale completed rows, permission-gated delete.
Source
import test;
import gebweb;
import db;
import json;
import feature_tour.app as appmod;
import feature_tour.handlers as handlers;
func buildApp(): dict<string, any> {
let conn = db.connect("sqlite", ":memory:");
let gw = gebweb.app([]);
gebweb.useApiKeyAuth(gw, appmod.CurrentUser, func(string key): ?appmod.CurrentUser {
if (key == "ada-key") {
return appmod.CurrentUser("u-1", "ada", "user", ["tasks.delete"]);
}
if (key == "guest-key") {
return appmod.CurrentUser("u-2", "guest", "user", []);
}
return null;
});
gebweb.useViews(gw, "templates");
gebweb.useJobs(gw, conn);
gebweb.useScheduler(gw);
gebweb.useEvents(gw);
let mailer = gebweb.memoryMailer();
gebweb.useMailer(gw, mailer);
gebweb.useStorage(gw, gebweb.memoryStorage());
let repo = appmod.TaskRepo(conn);
let tasksCtrl = appmod.TaskController(repo, gw);
let attCtrl = appmod.AttachmentController(repo, gw);
let dashCtrl = appmod.DashboardController(repo, gw);
let app = gebweb.app([tasksCtrl, attCtrl, dashCtrl]);
/* Re-apply runtime services to the new app instance. */
gebweb.useApiKeyAuth(app, appmod.CurrentUser, func(string key): ?appmod.CurrentUser {
if (key == "ada-key") {
return appmod.CurrentUser("u-1", "ada", "user", ["tasks.delete"]);
}
if (key == "guest-key") {
return appmod.CurrentUser("u-2", "guest", "user", []);
}
return null;
});
gebweb.useViews(app, "templates");
gebweb.useJobs(app, conn);
gebweb.useScheduler(app);
gebweb.useEvents(app);
gebweb.useMailer(app, mailer);
gebweb.useStorage(app, gebweb.memoryStorage());
gebweb.registerEventSubscribers(app, [handlers.TaskNotifier()]);
gebweb.registerJobHandlers(app, [handlers.TaskNotificationJob()]);
gebweb.registerScheduledTasks(app, [handlers.TaskCleanup(repo)]);
/* Rebuild controllers against the final app so the back-
* reference matches. */
let app2Tasks = appmod.TaskController(repo, app);
let app2Att = appmod.AttachmentController(repo, app);
let app2Dash = appmod.DashboardController(repo, app);
let app2 = gebweb.app([app2Tasks, app2Att, app2Dash]);
gebweb.useApiKeyAuth(app2, appmod.CurrentUser, func(string key): ?appmod.CurrentUser {
if (key == "ada-key") {
return appmod.CurrentUser("u-1", "ada", "user", ["tasks.delete"]);
}
if (key == "guest-key") {
return appmod.CurrentUser("u-2", "guest", "user", []);
}
return null;
});
gebweb.useViews(app2, "templates");
gebweb.useJobs(app2, conn);
gebweb.useScheduler(app2);
gebweb.useEvents(app2);
gebweb.useMailer(app2, mailer);
gebweb.useStorage(app2, gebweb.memoryStorage());
gebweb.registerEventSubscribers(app2, [handlers.TaskNotifier()]);
gebweb.registerJobHandlers(app2, [handlers.TaskNotificationJob()]);
return {"app": app2, "mailer": mailer, "repo": repo};
}
class Tasks13Test extends test.Test {
@test
func createAndListTask(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
let create = client.request("POST", "/tasks", {"title": "Write tests"}, {"X-API-Key": "ada-key"});
this.assertEquals(200, create.status);
let list = client.request("GET", "/tasks?size=10", null, {"X-API-Key": "ada-key"});
this.assertEquals(200, list.status);
let body = list.json() as dict<string, any>;
let items = body["items"] as list<any>;
this.assertEquals(1, items.length());
}
@test
func unauthRejected(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
let r = client.get("/tasks");
this.assertEquals(401, r.status);
}
@test
func completeTaskFiresMailerJob(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
let create = client.request("POST", "/tasks", {"title": "Ship it"}, {"X-API-Key": "ada-key"});
let task = create.json() as dict<string, any>;
let id = task["id"] as string;
let done = client.request("POST", "/tasks/" + id + "/complete", null, {"X-API-Key": "ada-key"});
this.assertEquals(200, done.status);
/* The event publishes synchronously which enqueues a job;
* runWorker drains it and the mailer captures the result. */
/* The chain is two-deep: the event subscriber enqueues
* "send-task-notification" which calls gebweb.send, which
* (because a jobs queue is registered) enqueues the
* "_gebweb_mailer_deliver" job. drainOnce processes both
* and returns when the queue is empty. */
gebweb.runWorker(ctx["app"], {"drainOnce": true});
let mailer = ctx["mailer"];
let sent = mailer.sent;
this.assertEquals(1, sent.length());
let m = sent[0] as dict<string, any>;
this.assertEquals("[email protected]", m["to"]);
this.assertTrue((m["subject"] as string).contains("Ship it"));
}
@test
func attachmentSavedToStorage(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
let create = client.request("POST", "/tasks", {"title": "Reviewed"}, {"X-API-Key": "ada-key"});
let id = (create.json() as dict<string, any>)["id"] as string;
let upload = client.multipart("/tasks/" + id + "/attachment",
{"file": {"filename": "notes.txt", "contentType": "text/plain", "content": "hi"}},
{"X-API-Key": "ada-key"}
);
this.assertEquals(200, upload.status);
let body = upload.json() as dict<string, any>;
this.assertTrue((body["name"] as string).startsWith("tasks/"));
}
@test
func deleteRequiresPermission(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
let create = client.request("POST", "/tasks", {"title": "X"}, {"X-API-Key": "ada-key"});
let id = (create.json() as dict<string, any>)["id"] as string;
/* guest-key has no tasks.delete permission. */
let denied = client.request("DELETE", "/tasks/" + id, null, {"X-API-Key": "guest-key"});
this.assertEquals(403, denied.status);
/* ada-key has it. */
let allowed = client.request("DELETE", "/tasks/" + id, null, {"X-API-Key": "ada-key"});
this.assertEquals(200, allowed.status);
}
@test
func dashboardRendersHtml(): void {
let ctx = buildApp();
let client = gebweb.TestClient(ctx["app"]);
client.request("POST", "/tasks", {"title": "Hello"}, {"X-API-Key": "ada-key"});
let r = client.request("GET", "/dashboard", null, {"X-API-Key": "ada-key"});
this.assertEquals(200, r.status);
let body = r.text();
this.assertTrue(body.contains("Hello"));
this.assertTrue(body.contains("ada"));
}
@test
func cleanupPurgesOldCompleted(): void {
let ctx = buildApp();
let repo = ctx["repo"] as appmod.TaskRepo;
/* Seed: one fresh + one stale completed task. */
let fresh = appmod.Task();
fresh.ownerId = "u-1"; fresh.title = "fresh"; fresh.status = "completed";
fresh.completedAt = 9_999_999_999;
repo.save(fresh);
let stale = appmod.Task();
stale.ownerId = "u-1"; stale.title = "stale"; stale.status = "completed";
stale.completedAt = 1;
repo.save(stale);
let removed = repo.purgeOldCompleted(1_000_000_000);
this.assertEquals(1, removed);
this.assertNotNull(repo.find(fresh.id));
this.assertNull(repo.find(stale.id));
}
}