Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/App/Yath/Server/Plack.pm
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ sub handle_request {
my $self = shift;
my ($env) = @_;

# Health check endpoint — bypass routing, sessions, and auth
if ($env->{PATH_INFO} eq '/health' && ($env->{REQUEST_METHOD} || '') eq 'GET') {
return $self->_health_check();
}

my $schema = $self->schema;
my $router = $self->router;
my $route = $router->match($env) || {};
Expand Down Expand Up @@ -304,6 +309,31 @@ sub handle_request {
}


sub _health_check {
my $self = shift;

my ($ok, $error);
eval {
my $dbh = $self->schema->storage->dbh;
$ok = $dbh->ping ? 1 : 0;
$error = "DB ping returned false" unless $ok;
1;
} or do {
$ok = 0;
$error = "$@";
chomp $error;
};

my $status = $ok ? 200 : 503;
my $body = $ok ? {ok => 1} : {ok => 0, error => $error};

my $res = resp($status);
$res->content_type('application/json');
$res->body(encode_json($body));
return $res->finalize;
}


__END__

=pod
Expand Down
142 changes: 140 additions & 2 deletions t/unit/App/Yath/Server/Plack.t
Original file line number Diff line number Diff line change
@@ -1,5 +1,143 @@
use Test2::V0 -target => 'App::Yath::Server::Plack';
use Test2::V0;

skip_all "write me";
BEGIN {
my $ok = eval { require App::Yath::Server::Plack; 1 };
skip_all "Server dependencies not installed" unless $ok;
}

use Test2::Harness::Util::JSON qw/decode_json/;
use App::Yath::Server::Plack;

subtest '_health_check returns 200 when DB ping succeeds' => sub {
my $mock_dbh = mock {} => (
add => [ping => sub { 1 }],
);
my $mock_storage = mock {} => (
add => [dbh => sub { $mock_dbh }],
);
my $mock_schema = mock {} => (
add => [
storage => sub { $mock_storage },
config => sub { 0 },
],
);
my $mock_config = mock {} => (
add => [schema => sub { $mock_schema }],
);

my $plack = App::Yath::Server::Plack->new(schema_config => $mock_config);

my $env = {
PATH_INFO => '/health',
REQUEST_METHOD => 'GET',
};
my $result = $plack->handle_request($env);

is($result->[0], 200, 'status is 200');

my $body = join('', @{$result->[2]});
my $data = decode_json($body);
is($data, {ok => 1}, 'body contains ok => 1');
};

subtest '_health_check returns 503 when DB ping fails' => sub {
my $mock_dbh = mock {} => (
add => [ping => sub { 0 }],
);
my $mock_storage = mock {} => (
add => [dbh => sub { $mock_dbh }],
);
my $mock_schema = mock {} => (
add => [
storage => sub { $mock_storage },
config => sub { 0 },
],
);
my $mock_config = mock {} => (
add => [schema => sub { $mock_schema }],
);

my $plack = App::Yath::Server::Plack->new(schema_config => $mock_config);

my $env = {
PATH_INFO => '/health',
REQUEST_METHOD => 'GET',
};
my $result = $plack->handle_request($env);

is($result->[0], 503, 'status is 503');

my $body = join('', @{$result->[2]});
my $data = decode_json($body);
is($data->{ok}, 0, 'ok is 0');
like($data->{error}, qr/ping/, 'error mentions ping');
};

subtest '_health_check returns 503 when DB connection throws' => sub {
my $mock_storage = mock {} => (
add => [dbh => sub { die "Connection refused\n" }],
);
my $mock_schema = mock {} => (
add => [
storage => sub { $mock_storage },
config => sub { 0 },
],
);
my $mock_config = mock {} => (
add => [schema => sub { $mock_schema }],
);

my $plack = App::Yath::Server::Plack->new(schema_config => $mock_config);

my $env = {
PATH_INFO => '/health',
REQUEST_METHOD => 'GET',
};
my $result = $plack->handle_request($env);

is($result->[0], 503, 'status is 503');

my $body = join('', @{$result->[2]});
my $data = decode_json($body);
is($data->{ok}, 0, 'ok is 0');
like($data->{error}, qr/Connection refused/, 'error captures exception message');
};

subtest '/health only responds to GET' => sub {
my $health_called = 0;
my $mock_dbh = mock {} => (
add => [ping => sub { $health_called++; 1 }],
);
my $mock_storage = mock {} => (
add => [dbh => sub { $mock_dbh }],
);
my $mock_schema = mock {} => (
add => [
storage => sub { $mock_storage },
config => sub { 0 },
],
);
my $mock_config = mock {} => (
add => [schema => sub { $mock_schema }],
);

my $plack = App::Yath::Server::Plack->new(schema_config => $mock_config);

# GET /health should trigger the health check
my $get_result = $plack->handle_request({
PATH_INFO => '/health',
REQUEST_METHOD => 'GET',
});
is($get_result->[0], 200, 'GET /health returns 200');
is($health_called, 1, 'health check was invoked for GET');

# POST /health should NOT trigger the health check — it falls through
$health_called = 0;
my $post_result = eval { $plack->handle_request({
PATH_INFO => '/health',
REQUEST_METHOD => 'POST',
}) };
is($health_called, 0, 'health check was NOT invoked for POST');
};

done_testing;
Loading