Initial phase 1 baseline implementation
Some checks failed
CI / Backend (push) Failing after 4s
CI / Frontend (push) Successful in 11s
CI / Contracts and repository policy (push) Failing after 5s
CI / Container (push) Has been skipped

This commit is contained in:
Elijah 2026-07-16 19:14:01 -07:00
parent 099c53badf
commit 077cf7601a
29 changed files with 1339 additions and 39 deletions

View file

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package dbgen
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View file

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: health.sql
package dbgen
import (
"context"
)
const databaseHealth = `-- name: DatabaseHealth :one
SELECT 1::BIGINT AS result
`
func (q *Queries) DatabaseHealth(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, databaseHealth)
var result int64
err := row.Scan(&result)
return result, err
}
const ownerExists = `-- name: OwnerExists :one
SELECT EXISTS (SELECT 1 FROM users)::BOOLEAN AS exists
`
func (q *Queries) OwnerExists(ctx context.Context) (bool, error) {
row := q.db.QueryRow(ctx, ownerExists)
var exists bool
err := row.Scan(&exists)
return exists, err
}

View file

@ -0,0 +1,266 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package dbgen
import (
"net/netip"
"github.com/jackc/pgx/v5/pgtype"
)
type ApiToken struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
ClientInstanceID pgtype.UUID `db:"client_instance_id" json:"client_instance_id"`
Name string `db:"name" json:"name"`
TokenHash []byte `db:"token_hash" json:"token_hash"`
Actions []string `db:"actions" json:"actions"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
LastUsedAt pgtype.Timestamptz `db:"last_used_at" json:"last_used_at"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
RevokedAt pgtype.Timestamptz `db:"revoked_at" json:"revoked_at"`
}
type AuditEvent struct {
Sequence int64 `db:"sequence" json:"sequence"`
EventID pgtype.UUID `db:"event_id" json:"event_id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
Actor []byte `db:"actor" json:"actor"`
Action string `db:"action" json:"action"`
ResourceType *string `db:"resource_type" json:"resource_type"`
ResourceID pgtype.UUID `db:"resource_id" json:"resource_id"`
Outcome string `db:"outcome" json:"outcome"`
RequestID pgtype.UUID `db:"request_id" json:"request_id"`
OperationID pgtype.UUID `db:"operation_id" json:"operation_id"`
Details []byte `db:"details" json:"details"`
OccurredAt pgtype.Timestamptz `db:"occurred_at" json:"occurred_at"`
}
type Blob struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
ObjectID pgtype.UUID `db:"object_id" json:"object_id"`
Sha256 []byte `db:"sha256" json:"sha256"`
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
State string `db:"state" json:"state"`
VerifiedAt pgtype.Timestamptz `db:"verified_at" json:"verified_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type Change struct {
Sequence int64 `db:"sequence" json:"sequence"`
EventID pgtype.UUID `db:"event_id" json:"event_id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
EventType string `db:"event_type" json:"event_type"`
SchemaVersion int32 `db:"schema_version" json:"schema_version"`
NodeID pgtype.UUID `db:"node_id" json:"node_id"`
NodeRevision *int64 `db:"node_revision" json:"node_revision"`
ContentRevision *int64 `db:"content_revision" json:"content_revision"`
SubtreeEffect bool `db:"subtree_effect" json:"subtree_effect"`
ResultingState []byte `db:"resulting_state" json:"resulting_state"`
Tombstone []byte `db:"tombstone" json:"tombstone"`
PreviousState []byte `db:"previous_state" json:"previous_state"`
MutationOrigin []byte `db:"mutation_origin" json:"mutation_origin"`
CommittedAt pgtype.Timestamptz `db:"committed_at" json:"committed_at"`
}
type ClientInstance struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
DisplayName string `db:"display_name" json:"display_name"`
ClientType string `db:"client_type" json:"client_type"`
Platform *string `db:"platform" json:"platform"`
SoftwareVersion *string `db:"software_version" json:"software_version"`
ProtocolVersion *string `db:"protocol_version" json:"protocol_version"`
LastSeenAt pgtype.Timestamptz `db:"last_seen_at" json:"last_seen_at"`
RevokedAt pgtype.Timestamptz `db:"revoked_at" json:"revoked_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type CommitIntent struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
OperationID pgtype.UUID `db:"operation_id" json:"operation_id"`
NodeID pgtype.UUID `db:"node_id" json:"node_id"`
ParentID pgtype.UUID `db:"parent_id" json:"parent_id"`
ExpectedNodeRevision *int64 `db:"expected_node_revision" json:"expected_node_revision"`
TargetName string `db:"target_name" json:"target_name"`
TargetNameKey string `db:"target_name_key" json:"target_name_key"`
ObjectID pgtype.UUID `db:"object_id" json:"object_id"`
Sha256 []byte `db:"sha256" json:"sha256"`
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
State string `db:"state" json:"state"`
PromotedAt pgtype.Timestamptz `db:"promoted_at" json:"promoted_at"`
CompletedAt pgtype.Timestamptz `db:"completed_at" json:"completed_at"`
FailureCode *string `db:"failure_code" json:"failure_code"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type CredentialRootGrant struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
SessionID pgtype.UUID `db:"session_id" json:"session_id"`
ApiTokenID pgtype.UUID `db:"api_token_id" json:"api_token_id"`
RootNodeID pgtype.UUID `db:"root_node_id" json:"root_node_id"`
Actions []string `db:"actions" json:"actions"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type DriveMetadatum struct {
Singleton bool `db:"singleton" json:"singleton"`
InstalledAt pgtype.Timestamptz `db:"installed_at" json:"installed_at"`
}
type ExportItem struct {
ExportLeaseID pgtype.UUID `db:"export_lease_id" json:"export_lease_id"`
NodeID pgtype.UUID `db:"node_id" json:"node_id"`
BlobID pgtype.UUID `db:"blob_id" json:"blob_id"`
NodeRevision int64 `db:"node_revision" json:"node_revision"`
RelativePath string `db:"relative_path" json:"relative_path"`
}
type ExportLease struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
OperationID pgtype.UUID `db:"operation_id" json:"operation_id"`
State string `db:"state" json:"state"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
ReleasedAt pgtype.Timestamptz `db:"released_at" json:"released_at"`
}
type IdempotencyRecord struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
CredentialKind string `db:"credential_kind" json:"credential_kind"`
CredentialID pgtype.UUID `db:"credential_id" json:"credential_id"`
ClientInstanceID pgtype.UUID `db:"client_instance_id" json:"client_instance_id"`
IdempotencyKey string `db:"idempotency_key" json:"idempotency_key"`
RequestFingerprint []byte `db:"request_fingerprint" json:"request_fingerprint"`
OperationType string `db:"operation_type" json:"operation_type"`
State string `db:"state" json:"state"`
ResponseReference []byte `db:"response_reference" json:"response_reference"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
CompletedAt pgtype.Timestamptz `db:"completed_at" json:"completed_at"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
}
type Job struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
JobType string `db:"job_type" json:"job_type"`
State string `db:"state" json:"state"`
Payload []byte `db:"payload" json:"payload"`
Progress []byte `db:"progress" json:"progress"`
AttemptCount int64 `db:"attempt_count" json:"attempt_count"`
MaxAttempts int64 `db:"max_attempts" json:"max_attempts"`
LeaseHolder pgtype.UUID `db:"lease_holder" json:"lease_holder"`
LeaseExpiresAt pgtype.Timestamptz `db:"lease_expires_at" json:"lease_expires_at"`
HeartbeatAt pgtype.Timestamptz `db:"heartbeat_at" json:"heartbeat_at"`
NextAttemptAt pgtype.Timestamptz `db:"next_attempt_at" json:"next_attempt_at"`
CancelRequestedAt pgtype.Timestamptz `db:"cancel_requested_at" json:"cancel_requested_at"`
StartedAt pgtype.Timestamptz `db:"started_at" json:"started_at"`
CompletedAt pgtype.Timestamptz `db:"completed_at" json:"completed_at"`
LastErrorCode *string `db:"last_error_code" json:"last_error_code"`
LastErrorDetail *string `db:"last_error_detail" json:"last_error_detail"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type Node struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
ParentID pgtype.UUID `db:"parent_id" json:"parent_id"`
Name string `db:"name" json:"name"`
NameKey string `db:"name_key" json:"name_key"`
NodeType string `db:"node_type" json:"node_type"`
CurrentBlobID pgtype.UUID `db:"current_blob_id" json:"current_blob_id"`
SizeBytes int64 `db:"size_bytes" json:"size_bytes"`
MimeType *string `db:"mime_type" json:"mime_type"`
NodeRevision int64 `db:"node_revision" json:"node_revision"`
ContentRevision int64 `db:"content_revision" json:"content_revision"`
ContentModifiedAt pgtype.Timestamptz `db:"content_modified_at" json:"content_modified_at"`
MetadataUpdatedAt pgtype.Timestamptz `db:"metadata_updated_at" json:"metadata_updated_at"`
Favorite bool `db:"favorite" json:"favorite"`
TrashRootAt pgtype.Timestamptz `db:"trash_root_at" json:"trash_root_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type RecoveryCode struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
CodeHash []byte `db:"code_hash" json:"code_hash"`
UsedAt pgtype.Timestamptz `db:"used_at" json:"used_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type ReplacementRecovery struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
NodeID pgtype.UUID `db:"node_id" json:"node_id"`
BlobID pgtype.UUID `db:"blob_id" json:"blob_id"`
ReplacedAt pgtype.Timestamptz `db:"replaced_at" json:"replaced_at"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
RestoredAt pgtype.Timestamptz `db:"restored_at" json:"restored_at"`
}
type ResourceLease struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
Purpose string `db:"purpose" json:"purpose"`
ResourceKind string `db:"resource_kind" json:"resource_kind"`
ResourceID pgtype.UUID `db:"resource_id" json:"resource_id"`
HolderID pgtype.UUID `db:"holder_id" json:"holder_id"`
AcquiredAt pgtype.Timestamptz `db:"acquired_at" json:"acquired_at"`
HeartbeatAt pgtype.Timestamptz `db:"heartbeat_at" json:"heartbeat_at"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
ReleasedAt pgtype.Timestamptz `db:"released_at" json:"released_at"`
}
type Session struct {
ID pgtype.UUID `db:"id" json:"id"`
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
ClientInstanceID pgtype.UUID `db:"client_instance_id" json:"client_instance_id"`
TokenHash []byte `db:"token_hash" json:"token_hash"`
CsrfTokenHash []byte `db:"csrf_token_hash" json:"csrf_token_hash"`
RemoteAddress *netip.Addr `db:"remote_address" json:"remote_address"`
UserAgent *string `db:"user_agent" json:"user_agent"`
AuthenticatedAt pgtype.Timestamptz `db:"authenticated_at" json:"authenticated_at"`
LastSeenAt pgtype.Timestamptz `db:"last_seen_at" json:"last_seen_at"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
RevokedAt pgtype.Timestamptz `db:"revoked_at" json:"revoked_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type Setting struct {
OwnerID pgtype.UUID `db:"owner_id" json:"owner_id"`
SettingKey string `db:"setting_key" json:"setting_key"`
Value []byte `db:"value" json:"value"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type TotpCredential struct {
UserID pgtype.UUID `db:"user_id" json:"user_id"`
EncryptedSecret []byte `db:"encrypted_secret" json:"encrypted_secret"`
EnabledAt pgtype.Timestamptz `db:"enabled_at" json:"enabled_at"`
VerifiedStep *int64 `db:"verified_step" json:"verified_step"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type User struct {
ID pgtype.UUID `db:"id" json:"id"`
Username string `db:"username" json:"username"`
UsernameKey string `db:"username_key" json:"username_key"`
DisplayName string `db:"display_name" json:"display_name"`
PasswordHash string `db:"password_hash" json:"password_hash"`
RootNodeID pgtype.UUID `db:"root_node_id" json:"root_node_id"`
SetupCompletedAt pgtype.Timestamptz `db:"setup_completed_at" json:"setup_completed_at"`
PasswordChangedAt pgtype.Timestamptz `db:"password_changed_at" json:"password_changed_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}

View file

@ -0,0 +1,16 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package dbgen
import (
"context"
)
type Querier interface {
DatabaseHealth(ctx context.Context) (int64, error)
OwnerExists(ctx context.Context) (bool, error)
}
var _ Querier = (*Queries)(nil)

View file

@ -0,0 +1,56 @@
package postgres
import (
"errors"
"fmt"
"net/url"
"drive.local/drivev2/db/migrations"
"github.com/golang-migrate/migrate/v4"
pgx5migrate "github.com/golang-migrate/migrate/v4/database/pgx/v5"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
// Migrate applies every pending forward migration using an advisory lock.
func Migrate(databaseURL string) (returnErr error) {
sourceDriver, err := iofs.New(migrations.Files, ".")
if err != nil {
return fmt.Errorf("open embedded migrations: %w", err)
}
migrationURL, err := url.Parse(databaseURL)
if err != nil {
return fmt.Errorf("parse database URL for migrations: %w", err)
}
migrationURL.Scheme = "pgx5"
query := migrationURL.Query()
query.Set("x-multi-statement", "true")
query.Set("x-statement-timeout", "30000")
if query.Get("connect_timeout") == "" {
query.Set("connect_timeout", "30")
}
migrationURL.RawQuery = query.Encode()
databaseDriver, err := (&pgx5migrate.Postgres{}).Open(migrationURL.String())
if err != nil {
return fmt.Errorf("open migration database: %w", err)
}
runner, err := migrate.NewWithInstance("iofs", sourceDriver, "pgx5", databaseDriver)
if err != nil {
return errors.Join(
fmt.Errorf("create migration runner: %w", err),
sourceDriver.Close(),
databaseDriver.Close(),
)
}
defer func() {
sourceErr, databaseErr := runner.Close()
returnErr = errors.Join(returnErr, sourceErr, databaseErr)
}()
if err := runner.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return fmt.Errorf("apply database migrations: %w", err)
}
return nil
}

View file

@ -0,0 +1,41 @@
package postgres
import (
"context"
"os"
"testing"
"time"
)
func TestMigrationsAndStore(t *testing.T) {
databaseURL := os.Getenv("DRIVE_TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("DRIVE_TEST_DATABASE_URL is not set")
}
if err := Migrate(databaseURL); err != nil {
t.Fatalf("first migration run failed: %v", err)
}
if err := Migrate(databaseURL); err != nil {
t.Fatalf("second migration run failed: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatalf("Open returned an error: %v", err)
}
defer store.Close()
if err := store.DatabaseHealth(ctx); err != nil {
t.Fatalf("DatabaseHealth returned an error: %v", err)
}
initialized, err := store.OwnerExists(ctx)
if err != nil {
t.Fatalf("OwnerExists returned an error: %v", err)
}
if initialized {
t.Fatal("OwnerExists = true on a fresh database, want false")
}
}

View file

@ -0,0 +1,46 @@
package postgres
import (
"context"
"fmt"
dbgen "drive.local/drivev2/internal/adapters/postgres/generated"
"github.com/jackc/pgx/v5/pgxpool"
)
// Store owns the PostgreSQL connection pool and generated repositories.
type Store struct {
pool *pgxpool.Pool
queries *dbgen.Queries
}
func Open(ctx context.Context, databaseURL string) (*Store, error) {
poolConfig, err := pgxpool.ParseConfig(databaseURL)
if err != nil {
return nil, fmt.Errorf("parse database configuration: %w", err)
}
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
return nil, fmt.Errorf("create database pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping database: %w", err)
}
return &Store{pool: pool, queries: dbgen.New(pool)}, nil
}
func (s *Store) Close() {
s.pool.Close()
}
func (s *Store) DatabaseHealth(ctx context.Context) error {
_, err := s.queries.DatabaseHealth(ctx)
return err
}
func (s *Store) OwnerExists(ctx context.Context) (bool, error) {
return s.queries.OwnerExists(ctx)
}

View file

@ -0,0 +1,41 @@
package application
import (
"context"
"fmt"
)
type SystemRepository interface {
DatabaseHealth(context.Context) error
OwnerExists(context.Context) (bool, error)
}
type SetupStatus struct {
Initialized bool `json:"initialized"`
Phase string `json:"phase"`
Version string `json:"version"`
}
type SystemService struct {
repository SystemRepository
version string
}
func NewSystemService(repository SystemRepository, version string) *SystemService {
return &SystemService{repository: repository, version: version}
}
func (s *SystemService) Readiness(ctx context.Context) error {
if err := s.repository.DatabaseHealth(ctx); err != nil {
return fmt.Errorf("database health: %w", err)
}
return nil
}
func (s *SystemService) SetupStatus(ctx context.Context) (SetupStatus, error) {
initialized, err := s.repository.OwnerExists(ctx)
if err != nil {
return SetupStatus{}, fmt.Errorf("read owner setup state: %w", err)
}
return SetupStatus{Initialized: initialized, Phase: "phase-1", Version: s.version}, nil
}

View file

@ -0,0 +1,44 @@
package application
import (
"context"
"errors"
"testing"
)
type systemRepositoryStub struct {
healthErr error
ownerExists bool
ownerErr error
}
func (s systemRepositoryStub) DatabaseHealth(context.Context) error {
return s.healthErr
}
func (s systemRepositoryStub) OwnerExists(context.Context) (bool, error) {
return s.ownerExists, s.ownerErr
}
func TestSystemServiceReadinessPropagatesDatabaseFailure(t *testing.T) {
t.Parallel()
want := errors.New("database unavailable")
service := NewSystemService(systemRepositoryStub{healthErr: want}, "test")
if err := service.Readiness(context.Background()); !errors.Is(err, want) {
t.Fatalf("Readiness error = %v, want wrapped %v", err, want)
}
}
func TestSystemServiceSetupStatusUsesPersistentOwnerState(t *testing.T) {
t.Parallel()
service := NewSystemService(systemRepositoryStub{ownerExists: true}, "test")
status, err := service.SetupStatus(context.Background())
if err != nil {
t.Fatalf("SetupStatus returned an error: %v", err)
}
if !status.Initialized || status.Phase != "phase-1" || status.Version != "test" {
t.Fatalf("unexpected setup status: %#v", status)
}
}

View file

@ -21,12 +21,16 @@ func TestImportBoundaries(t *testing.T) {
}{
{
directory: "internal/domain",
forbidden: []string{"net/http", "database/sql", "github.com/jackc/pgx", "/internal/adapters/", "/internal/transport/"},
forbidden: []string{"net/http", "database/sql", "github.com/jackc/pgx", "os", "io/fs", "path/filepath", "/internal/adapters/", "/internal/transport/"},
},
{
directory: "internal/transport",
forbidden: []string{"database/sql", "github.com/jackc/pgx", "os"},
},
{
directory: "internal/adapters/postgres",
forbidden: []string{"/internal/transport/"},
},
}
for _, rule := range rules {
@ -70,8 +74,15 @@ func assertImportAllowed(t *testing.T, path string, spec *ast.ImportSpec, forbid
t.Fatalf("unquote import in %s: %v", path, err)
}
for _, pattern := range forbidden {
if importPath == pattern || strings.Contains(importPath, pattern) {
if forbiddenImportMatches(importPath, pattern) {
t.Errorf("%s imports forbidden package %q", path, importPath)
}
}
}
func forbiddenImportMatches(importPath, pattern string) bool {
if strings.HasPrefix(pattern, "/") {
return strings.Contains(importPath, pattern)
}
return importPath == pattern || strings.HasPrefix(importPath, pattern+"/")
}

View file

@ -7,6 +7,7 @@ import (
"net/http"
"time"
"drive.local/drivev2/internal/application"
"drive.local/drivev2/internal/config"
)
@ -16,15 +17,16 @@ type Server struct {
httpServer *http.Server
}
func NewServer(cfg config.Config, version string, logger *slog.Logger, frontend http.Handler) *Server {
type SystemService interface {
Readiness(context.Context) error
SetupStatus(context.Context) (application.SetupStatus, error)
}
func NewServer(cfg config.Config, logger *slog.Logger, system SystemService, frontend http.Handler) *Server {
mux := http.NewServeMux()
mux.HandleFunc("GET /health/live", jsonResponse(http.StatusOK, map[string]string{"status": "live"}))
mux.HandleFunc("GET /health/ready", jsonResponse(http.StatusOK, map[string]string{"status": "ready"}))
mux.HandleFunc("GET /api/v1/setup/status", jsonResponse(http.StatusOK, map[string]any{
"initialized": false,
"phase": "foundation",
"version": version,
}))
mux.HandleFunc("GET /health/ready", readinessHandler(logger, system))
mux.HandleFunc("GET /api/v1/setup/status", setupStatusHandler(logger, system))
mux.Handle("/", frontend)
return &Server{httpServer: &http.Server{
@ -35,6 +37,29 @@ func NewServer(cfg config.Config, version string, logger *slog.Logger, frontend
}}
}
func readinessHandler(logger *slog.Logger, system SystemService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := system.Readiness(r.Context()); err != nil {
logger.Error("readiness check failed", "error", err)
problemResponse(w, http.StatusServiceUnavailable, "dependency_unavailable", "A required dependency is unavailable")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}
}
func setupStatusHandler(logger *slog.Logger, system SystemService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
status, err := system.SetupStatus(r.Context())
if err != nil {
logger.Error("setup status failed", "error", err)
problemResponse(w, http.StatusServiceUnavailable, "dependency_unavailable", "A required dependency is unavailable")
return
}
writeJSON(w, http.StatusOK, status)
}
}
func (s *Server) ListenAndServe() error {
return s.httpServer.ListenAndServe()
}
@ -44,13 +69,30 @@ func (s *Server) Shutdown(ctx context.Context) error {
}
func jsonResponse(status int, payload any) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(payload); err != nil {
slog.Error("encode response", "error", err)
}
return func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, status, payload) }
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(payload); err != nil {
slog.Error("encode response", "error", err)
}
}
func problemResponse(w http.ResponseWriter, status int, code, detail string) {
w.Header().Set("Content-Type", "application/problem+json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(map[string]any{
"type": "about:blank",
"title": http.StatusText(status),
"status": status,
"detail": detail,
"code": code,
}); err != nil {
slog.Error("encode problem response", "error", err)
}
}

View file

@ -1,6 +1,8 @@
package httpapi
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
@ -8,16 +10,31 @@ import (
"strings"
"testing"
"drive.local/drivev2/internal/application"
"drive.local/drivev2/internal/config"
)
type systemServiceStub struct {
readinessErr error
setupStatus application.SetupStatus
setupErr error
}
func (s systemServiceStub) Readiness(context.Context) error {
return s.readinessErr
}
func (s systemServiceStub) SetupStatus(context.Context) (application.SetupStatus, error) {
return s.setupStatus, s.setupErr
}
func TestLiveHealth(t *testing.T) {
t.Parallel()
server := NewServer(
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
"test",
slog.New(slog.NewTextHandler(io.Discard, nil)),
systemServiceStub{},
http.NotFoundHandler(),
)
request := httptest.NewRequest(http.MethodGet, "/health/live", nil)
@ -32,3 +49,50 @@ func TestLiveHealth(t *testing.T) {
t.Fatalf("unexpected body: %s", recorder.Body.String())
}
}
func TestReadinessReportsDependencyFailure(t *testing.T) {
t.Parallel()
server := NewServer(
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
slog.New(slog.NewTextHandler(io.Discard, nil)),
systemServiceStub{readinessErr: errors.New("database unavailable")},
http.NotFoundHandler(),
)
request := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
recorder := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusServiceUnavailable)
}
if contentType := recorder.Header().Get("Content-Type"); contentType != "application/problem+json" {
t.Fatalf("Content-Type = %q, want application/problem+json", contentType)
}
if !strings.Contains(recorder.Body.String(), `"code":"dependency_unavailable"`) {
t.Fatalf("unexpected body: %s", recorder.Body.String())
}
}
func TestSetupStatusUsesApplicationService(t *testing.T) {
t.Parallel()
server := NewServer(
config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()},
slog.New(slog.NewTextHandler(io.Discard, nil)),
systemServiceStub{setupStatus: application.SetupStatus{Initialized: true, Phase: "phase-1", Version: "test"}},
http.NotFoundHandler(),
)
request := httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil)
recorder := httptest.NewRecorder()
server.httpServer.Handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
}
if !strings.Contains(recorder.Body.String(), `"initialized":true`) {
t.Fatalf("unexpected body: %s", recorder.Body.String())
}
}