zynagent-first application framework

Add an application test#

This recipe tests the article API and Article.validate_title created by recipes/add-database-backed-resource. Complete that recipe first, including its migration and routes. Tests use isolated fixture databases, not development data.

1. Create a reachable test module#

Create src/article_tests.zig:

// src/article_tests.zig
const std = @import("std");
const zyn = @import("zyn");
const Article = @import("resources/Article.zig");

test "article titles reject blank input" {
    try std.testing.expectError(error.InvalidTitle, Article.validate_title(" \t\n"));
    try Article.validate_title("Example article");
}

test "GET article returns the stored representation" {
    var app = try zyn.testing.start(std.testing.io, std.testing.allocator, .{});
    defer app.deinit();

    const row = (try app.db.one(
        struct { id: []const u8 },
        "INSERT INTO articles (title) VALUES ($1) RETURNING id::text AS id",
        .{"Example article"},
    )).?;
    const path = try zyn.url(std.testing.allocator, "/articles/:id", .{ .id = row.id });
    defer std.testing.allocator.free(path);

    var response = try app.request(.GET, path, .{});
    defer response.deinit();
    try std.testing.expectEqual(.ok, response.status);
    try std.testing.expect(std.mem.startsWith(
        u8,
        response.header("Content-Type").?,
        "application/json",
    ));
    const article = try std.json.parseFromSlice(Article, std.testing.allocator, response.body, .{});
    defer article.deinit();
    try std.testing.expectEqualStrings(row.id, article.value.id);
    try std.testing.expectEqualStrings("Example article", article.value.title);
}

test "POST article validates its payload and returns the created representation" {
    var app = try zyn.testing.start(std.testing.io, std.testing.allocator, .{});
    defer app.deinit();
    const headers: []const std.http.Header = &.{
        .{ .name = "Content-Type", .value = "application/json" },
        .{ .name = "Sec-Fetch-Site", .value = "same-origin" },
    };

    var created = try app.request(.POST, "/articles", .{
        .headers = headers,
        .body = "{\"title\":\"Created article\"}",
    });
    defer created.deinit();
    try std.testing.expectEqual(.created, created.status);
    const article = try std.json.parseFromSlice(Article, std.testing.allocator, created.body, .{});
    defer article.deinit();
    try std.testing.expect(Article.valid_id(article.value.id));
    try std.testing.expectEqualStrings("Created article", article.value.title);

    var invalid = try app.request(.POST, "/articles", .{
        .headers = headers,
        .body = "{\"title\":\"   \"}",
    });
    defer invalid.deinit();
    try std.testing.expectEqual(.unprocessable_entity, invalid.status);

    var missing = try app.request(.GET, "/articles/00000000-0000-0000-0000-000000000000", .{});
    defer missing.deinit();
    try std.testing.expectEqual(.not_found, missing.status);

    var malformed = try app.request(.GET, "/articles/not-a-uuid", .{});
    defer malformed.deinit();
    try std.testing.expectEqual(.bad_request, malformed.status);
}

The first test needs no application fixture. The HTTP tests start managed applications with separate databases and listeners. Finish all requests and release their responses before app.deinit.

2. Import the test module#

Append this declaration to src/app.zig, preserving its application exports:

// src/app.zig: exports
pub const article_tests = @import("article_tests.zig");

An unreferenced standalone file is not discovered. A public module export makes these tests reachable through the application graph.

3. Run and inspect the tests#

zyn test --list --filter article
zyn test --filter article
zyn check

The listing and results should contain all three test names above. Changing the returned article's ID or title must fail the representation assertion; a 200 status alone does not verify the response body.

Unsafe HTTP tests supply Sec-Fetch-Site because the routes retain CSRF protection. Use managed jobs and notifications to perform deferred work synchronously instead of starting a worker in a test. See framework/testing for fixture database, storage, and notification operations.