Skip to content

Commit d06fa1f

Browse files
committed
Enhance testing harness and authz coverage
1 parent ed39e8f commit d06fa1f

File tree

8 files changed

+190
-22
lines changed

8 files changed

+190
-22
lines changed

Makefile

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
PROJECT_NAME := keep
2-
3-
.PHONY: all tidy build test lint format lint-go lint-python format-go format-python docker-up docker-down docker-logs db-migrate opa-test cert-refresh
2+
VENV ?= .venv
3+
VENV_BIN := $(VENV)/bin
4+
PYTHON_BIN := python3
5+
PIP_BIN := pip
6+
FLAKE8_CMD := $(PYTHON_BIN) -m flake8
7+
MYPY_CMD := $(PYTHON_BIN) -m mypy
8+
BLACK_CMD := $(PYTHON_BIN) -m black
9+
ISORT_CMD := $(PYTHON_BIN) -m isort
10+
PYTEST_CMD := $(PYTHON_BIN) -m pytest
11+
GOLANGCI_LINT ?= golangci-lint
12+
13+
ifneq ($(wildcard $(VENV_BIN)/python3),)
14+
PYTHON_BIN := $(VENV_BIN)/python3
15+
PIP_BIN := $(VENV_BIN)/pip
16+
FLAKE8_BIN := $(VENV_BIN)/flake8
17+
MYPY_BIN := $(VENV_BIN)/mypy
18+
BLACK_BIN := $(VENV_BIN)/black
19+
ISORT_BIN := $(VENV_BIN)/isort
20+
PYTEST_CMD := $(PYTHON_BIN) -m pytest
21+
endif
22+
23+
.PHONY: all tidy build test lint format lint-go lint-python format-go format-python docker-up docker-down docker-logs db-migrate opa-test cert-refresh setup-venv
424

525
all: build
626

@@ -12,7 +32,7 @@ build:
1232

1333
test:
1434
go test ./...
15-
python3 -m pytest
35+
$(PYTEST_CMD)
1636

1737
smoke:
1838
COMPOSE_FILE=docker-compose.yml ./scripts/smoke-tests.sh
@@ -27,21 +47,21 @@ format-go:
2747

2848
format-python:
2949
@echo "Formatting Python code..."
30-
black app/
31-
isort app/
50+
$(BLACK_CMD) app/
51+
$(ISORT_CMD) app/
3252

3353
# Linting targets
3454
lint: lint-go lint-python
3555

3656
lint-go:
3757
@echo "Linting Go code..."
3858
go mod download
39-
golangci-lint run
59+
$(GOLANGCI_LINT) run
4060

4161
lint-python:
4262
@echo "Linting Python code..."
43-
flake8 app/
44-
mypy app/ --ignore-missing-imports
63+
$(FLAKE8_CMD) app/
64+
$(MYPY_CMD) app/ --ignore-missing-imports
4565

4666
docker-up:
4767
docker compose up --build -d
@@ -74,7 +94,12 @@ install-tools:
7494
go install golang.org/x/tools/cmd/goimports@latest
7595
go install golang.org/x/vuln/cmd/govulncheck@latest
7696
@echo "Installing Python tools..."
77-
pip install black flake8 isort mypy
97+
$(PIP_BIN) install black flake8 isort mypy
98+
99+
setup-venv:
100+
python3 -m venv $(VENV)
101+
$(VENV_BIN)/python3 -m pip install --upgrade pip
102+
$(VENV_BIN)/pip install -r app/requirements.txt
78103
dev-bootstrap:
79104
./scripts/dev-bootstrap.sh
80105

@@ -83,10 +108,10 @@ check-tools:
83108
@command -v golangci-lint >/dev/null 2>&1 || { echo "golangci-lint not found. Run 'make install-tools'"; exit 1; }
84109
@command -v goimports >/dev/null 2>&1 || { echo "goimports not found. Run 'make install-tools'"; exit 1; }
85110
@echo "Checking Python tools..."
86-
@command -v black >/dev/null 2>&1 || { echo "black not found. Run 'make install-tools'"; exit 1; }
87-
@command -v flake8 >/dev/null 2>&1 || { echo "flake8 not found. Run 'make install-tools'"; exit 1; }
88-
@command -v isort >/dev/null 2>&1 || { echo "isort not found. Run 'make install-tools'"; exit 1; }
89-
@command -v mypy >/dev/null 2>&1 || { echo "mypy not found. Run 'make install-tools'"; exit 1; }
111+
@$(BLACK_CMD) --version >/dev/null 2>&1 || { echo "black not available. Run 'make install-tools'"; exit 1; }
112+
@$(FLAKE8_CMD) --version >/dev/null 2>&1 || { echo "flake8 not available. Run 'make install-tools'"; exit 1; }
113+
@$(ISORT_CMD) --version >/dev/null 2>&1 || { echo "isort not available. Run 'make install-tools'"; exit 1; }
114+
@$(MYPY_CMD) --version >/dev/null 2>&1 || { echo "mypy not available. Run 'make install-tools'"; exit 1; }
90115
@echo "All tools are available!"
91116

92117
# CI/CD targets

app/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import os
2+
import sys
3+
4+
if any("pytest" in arg for arg in sys.argv):
5+
os.environ.setdefault("OTEL_SDK_DISABLED", "true")
6+
17
from .main import app # re-export for tests
28

39
__all__ = ["app"]

app/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import time
34
from contextlib import contextmanager
45
from typing import Any, Callable, Dict, Generator, Tuple, TypeVar, cast
@@ -9,7 +10,8 @@
910
from app.telemetry import setup as telemetry_setup
1011

1112
app = Flask(__name__)
12-
telemetry_setup(app, service_name="keep-app")
13+
if os.getenv("OTEL_SDK_DISABLED", "").lower() not in {"true", "1"}:
14+
telemetry_setup(app, service_name="keep-app")
1315

1416
F = TypeVar("F", bound=Callable[..., ResponseReturnValue])
1517

app/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ opentelemetry-api
66
opentelemetry-sdk
77
opentelemetry-exporter-otlp
88
opentelemetry-instrumentation-flask
9+
flake8
10+
mypy
11+
black
12+
isort

app/telemetry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def instrument_flask_app(app: object) -> None:
4545

4646

4747
def setup(app: object, service_name: str, environment: Optional[str] = None) -> None:
48+
if os.getenv("OTEL_SDK_DISABLED", "").lower() in {"true", "1"}:
49+
_logger.info("telemetry disabled via OTEL_SDK_DISABLED")
50+
return
51+
4852
environment = environment or os.getenv("APP_ENV", "dev")
4953
endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")
5054
insecure = os.getenv("OTEL_EXPORTER_OTLP_INSECURE", "true").lower() == "true"

app/tests/conftest.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import pytest
77

8+
os.environ.setdefault("OTEL_SDK_DISABLED", "true")
9+
810
TRANSIENT_CODES = {
911
"08000",
1012
"08001",
@@ -17,6 +19,10 @@
1719
}
1820

1921
DEFAULT_DSN = "postgresql://postgres:[email protected]:5432/app_test"
22+
# Set APP_TEST_REQUIRE_DB=1 to run Flask integration tests against a live
23+
# Postgres instance. By default the tests stub out database checks so they can
24+
# run in isolated environments such as CI.
25+
REQUIRE_DATABASE = os.getenv("APP_TEST_REQUIRE_DB") == "1"
2026

2127

2228
def _sqlstate(exc: Exception) -> Optional[str]:
@@ -63,6 +69,9 @@ def resolve_dsn() -> str:
6369

6470

6571
def wait_for_database(dsn: str, timeout: float = 30.0) -> None:
72+
if not REQUIRE_DATABASE:
73+
return
74+
6675
try:
6776
import psycopg
6877
except ImportError as exc: # pragma: no cover - developer misconfiguration
@@ -95,10 +104,4 @@ def wait_for_database(dsn: str, timeout: float = 30.0) -> None:
95104

96105
@pytest.fixture(scope="session", autouse=True)
97106
def _ensure_database_ready() -> None:
98-
dsn = resolve_dsn()
99-
try:
100-
wait_for_database(dsn)
101-
except TimeoutError as exc:
102-
pytest.skip(f"Skipping DB-backed tests: {exc}")
103-
except RuntimeError as exc:
104-
pytest.skip(f"Skipping DB-backed tests due to runtime error: {exc}")
107+
wait_for_database(resolve_dsn())

app/tests/test_main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import os
12
from typing import Any, Generator
23

34
import pytest
45

5-
from app.main import app
6+
os.environ.setdefault("OTEL_SDK_DISABLED", "true")
7+
8+
from app.main import app # noqa: E402
69

710
TestClient = Any
811

services/authz/server/server_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"encoding/json"
77
"net/http"
88
"net/http/httptest"
9+
"path/filepath"
910
"strings"
1011
"testing"
1112
"time"
1213

1314
"tailscale.com/tsnet"
1415

16+
"github.com/EvalOps/keep/pkg/pki"
1517
"github.com/EvalOps/keep/pkg/retry"
1618
)
1719

@@ -28,6 +30,8 @@ const (
2830
testInventoryHost = "test-inventory:8080"
2931
testOPAHost = "test-opa:8181"
3032
testAuthzPort = ":8443"
33+
testRetryAttempts = 2
34+
testZeroLength = 0
3135
)
3236

3337
// TestServer_healthHandler tests the health endpoint
@@ -489,6 +493,9 @@ func TestServer_lookupDevice(t *testing.T) {
489493
if result["id"] != expectedDevice["id"] || result["posture"] != expectedDevice["posture"] {
490494
t.Errorf("Expected device info %v, got %v", expectedDevice, result)
491495
}
496+
if score, ok := result["trust_score"].(int); !ok || score != 0 {
497+
t.Errorf("Expected trust_score 0 for non-JSON posture, got %v", result["trust_score"])
498+
}
492499
})
493500

494501
t.Run("handles empty device ID", func(t *testing.T) {
@@ -606,6 +613,120 @@ func TestServer_caHandler(t *testing.T) {
606613
// which we avoid in unit tests to keep them lightweight
607614
}
608615

616+
func TestNewInitializesServerState(t *testing.T) {
617+
tmpDir := t.TempDir()
618+
certPath := filepath.Join(tmpDir, "ca.pem")
619+
keyPath := filepath.Join(tmpDir, "ca-key.pem")
620+
621+
t.Setenv("OTEL_SDK_DISABLED", "true")
622+
623+
_, err := pki.LoadOrCreateCA(certPath, keyPath, "test-ca", time.Hour)
624+
if err != nil {
625+
t.Fatalf("Failed to create CA: %v", err)
626+
}
627+
628+
cfg := Config{
629+
HTTPAddr: "127.0.0.1:0",
630+
GoogleClientID: "client-id",
631+
OPAURL: "http://opa",
632+
InventoryAPI: "http://inventory",
633+
RootCAPath: certPath,
634+
TLSCertPath: certPath,
635+
TLSKeyPath: keyPath,
636+
RequestTimeout: time.Second,
637+
RetryMaxElapsed: defaultInitialInterval,
638+
RetryMaxAttempts: testRetryAttempts,
639+
}
640+
641+
srv, err := New(cfg)
642+
if err != nil {
643+
t.Fatalf("New returned error: %v", err)
644+
}
645+
t.Cleanup(func() {
646+
_ = srv.httpSrv.Close()
647+
})
648+
649+
if srv.cfg == nil {
650+
t.Fatal("server cfg should not be nil")
651+
}
652+
if srv.retryCfg == nil {
653+
t.Fatal("server retryCfg should not be nil")
654+
}
655+
if srv.state.started {
656+
t.Error("server should not be marked started")
657+
}
658+
if srv.httpSrv == nil {
659+
t.Fatal("http server should be initialized")
660+
}
661+
if !srv.state.useTLS {
662+
t.Error("expected TLS to be enabled when cert/key provided")
663+
}
664+
if len(srv.rootCAPEM) == testZeroLength {
665+
t.Error("expected root CA PEM to be cached")
666+
}
667+
if srv.tsServer != nil {
668+
t.Error("tailscale server should be nil when auth key not provided")
669+
}
670+
671+
if srv.cfg.GoogleClientID != cfg.GoogleClientID {
672+
t.Errorf("expected GoogleClientID %q, got %q", cfg.GoogleClientID, srv.cfg.GoogleClientID)
673+
}
674+
}
675+
676+
func TestSetupHTTPServerTLSAndPlain(t *testing.T) {
677+
tmpDir := t.TempDir()
678+
certPath := filepath.Join(tmpDir, "ca.pem")
679+
keyPath := filepath.Join(tmpDir, "ca-key.pem")
680+
681+
ca, err := pki.LoadOrCreateCA(certPath, keyPath, "test-ca", time.Hour)
682+
if err != nil {
683+
t.Fatalf("Failed to create CA: %v", err)
684+
}
685+
handler := http.NewServeMux()
686+
687+
t.Run("enables TLS when certs provided", func(t *testing.T) {
688+
cfg := Config{
689+
HTTPAddr: "127.0.0.1:0",
690+
TLSCertPath: certPath,
691+
TLSKeyPath: keyPath,
692+
RootCAPath: certPath,
693+
}
694+
695+
srv := &Server{}
696+
if err := srv.setupHTTPServer(cfg, handler, ca); err != nil {
697+
t.Fatalf("setupHTTPServer returned error: %v", err)
698+
}
699+
t.Cleanup(func() {
700+
_ = srv.httpSrv.Close()
701+
})
702+
703+
if srv.httpSrv == nil {
704+
t.Fatal("http server should be initialized")
705+
}
706+
if !srv.state.useTLS {
707+
t.Error("expected TLS to be enabled")
708+
}
709+
})
710+
711+
t.Run("disables TLS when certs missing", func(t *testing.T) {
712+
cfg := Config{
713+
HTTPAddr: "127.0.0.1:0",
714+
}
715+
716+
srv := &Server{}
717+
if err := srv.setupHTTPServer(cfg, handler, ca); err != nil {
718+
t.Fatalf("setupHTTPServer returned error: %v", err)
719+
}
720+
t.Cleanup(func() {
721+
_ = srv.httpSrv.Close()
722+
})
723+
724+
if srv.state.useTLS {
725+
t.Error("expected TLS to remain disabled")
726+
}
727+
})
728+
}
729+
609730
// TestServer_validateTailscaleAccess tests Tailscale network validation
610731
func TestServer_validateTailscaleAccess(t *testing.T) {
611732
server := createTestServer(t)

0 commit comments

Comments
 (0)