File size: 5,234 Bytes
857a91b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package api

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/agent-matrix/matrix-runtime/internal/config"
	"github.com/agent-matrix/matrix-runtime/internal/jobs"
)

func get(t *testing.T, srv *Server, path, token string) (*httptest.ResponseRecorder, map[string]any) {
	t.Helper()
	req := httptest.NewRequest(http.MethodGet, path, nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	rec := httptest.NewRecorder()
	srv.Handler().ServeHTTP(rec, req)
	var out map[string]any
	_ = json.Unmarshal(rec.Body.Bytes(), &out)
	return rec, out
}

func TestVersionAndReady(t *testing.T) {
	srv := testServer(t)
	rec, body := get(t, srv, "/v1/version", "")
	if rec.Code != http.StatusOK || body["name"] != "matrix-runtime" {
		t.Fatalf("version = %d %v", rec.Code, body)
	}
	rec, body = get(t, srv, "/v1/ready", "")
	// No store in testServer → not ready, with a store_unavailable warning.
	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("ready status = %d, want 503 (no store)", rec.Code)
	}
	if _, ok := body["checks"].(map[string]any); !ok {
		t.Fatalf("ready missing checks: %v", body)
	}
	if body["ready"] != false {
		t.Errorf("ready = %v, want false", body["ready"])
	}
}

func TestReadyHealthyWithStore(t *testing.T) {
	srv := authServer(t) // has a store + temp data dir, local-dev
	rec, body := get(t, srv, "/v1/ready", "")
	if rec.Code != http.StatusOK || body["ready"] != true {
		t.Fatalf("ready = %d %v", rec.Code, body)
	}
	// local-dev should surface a local_dev_mode warning.
	ws, _ := body["warnings"].([]any)
	found := false
	for _, w := range ws {
		if m, ok := w.(map[string]any); ok && m["code"] == "local_dev_mode" {
			found = true
		}
	}
	if !found {
		t.Errorf("expected local_dev_mode warning, got %v", ws)
	}
}

func TestProductionFailsClosedWithoutCreds(t *testing.T) {
	cfg := config.Defaults(config.ModeCustomerAgent) // production mode
	cfg.DataDir = t.TempDir()
	srv := NewServer(cfg, jobs.NewManager(cfg), nil)

	// Probes stay public.
	if rec, _ := get(t, srv, "/v1/health", ""); rec.Code != http.StatusOK {
		t.Fatalf("health should be public, got %d", rec.Code)
	}
	if rec, _ := get(t, srv, "/v1/version", ""); rec.Code != http.StatusOK {
		t.Fatalf("version should be public, got %d", rec.Code)
	}
	// A protected endpoint with no creds is rejected (fail-closed).
	if rec, _ := get(t, srv, "/v1/jobs", ""); rec.Code != http.StatusUnauthorized {
		t.Fatalf("protected endpoint = %d, want 401", rec.Code)
	}
}

func TestOperatorAPITokenGate(t *testing.T) {
	cfg := config.Defaults(config.ModeCustomerAgent)
	cfg.DataDir = t.TempDir()
	cfg.APIToken = "s3cret-operator-token"
	srv := NewServer(cfg, jobs.NewManager(cfg), nil)

	if rec, _ := get(t, srv, "/v1/jobs", ""); rec.Code != http.StatusUnauthorized {
		t.Fatalf("no token = %d, want 401", rec.Code)
	}
	if rec, _ := get(t, srv, "/v1/jobs", "wrong"); rec.Code != http.StatusUnauthorized {
		t.Fatalf("wrong token = %d, want 401", rec.Code)
	}
	if rec, _ := get(t, srv, "/v1/jobs", "s3cret-operator-token"); rec.Code == http.StatusUnauthorized {
		t.Fatalf("correct operator token should pass the auth gate, got 401")
	}
}

func TestMatrixShellGate(t *testing.T) {
	// Production mode → MatrixShell disabled by default → 403.
	cfg := config.Defaults(config.ModeCustomerAgent)
	cfg.DataDir = t.TempDir()
	cfg.APIToken = "tok"
	srv := NewServer(cfg, jobs.NewManager(cfg), nil)
	if rec, _ := get(t, srv, "/v1/matrixshell/status", "tok"); rec.Code != http.StatusForbidden {
		t.Fatalf("matrixshell (prod, disabled) = %d, want 403", rec.Code)
	}

	// Explicitly enabled → reachable (200).
	cfg2 := config.Defaults(config.ModeCustomerAgent)
	cfg2.DataDir = t.TempDir()
	cfg2.APIToken = "tok"
	cfg2.MatrixShellEnabled = true
	srv2 := NewServer(cfg2, jobs.NewManager(cfg2), nil)
	if rec, _ := get(t, srv2, "/v1/matrixshell/status", "tok"); rec.Code == http.StatusForbidden {
		t.Fatalf("matrixshell (enabled) should not be 403")
	}
}

func TestStorageEndpoint(t *testing.T) {
	srv := authServer(t) // has store + temp data dir, local-dev (no auth required)
	rec, body := get(t, srv, "/v1/system/storage", "")
	if rec.Code != http.StatusOK {
		t.Fatalf("storage = %d", rec.Code)
	}
	if _, ok := body["areas"].(map[string]any); !ok {
		t.Fatalf("missing areas: %v", body)
	}
	if _, ok := body["total_bytes"]; !ok {
		t.Errorf("missing total_bytes")
	}
}

func TestRateLimit(t *testing.T) {
	cfg := config.Defaults(config.ModeLocalDev)
	cfg.DataDir = t.TempDir()
	cfg.RateLimitRPM = 3
	srv := NewServer(cfg, jobs.NewManager(cfg), nil)

	// GET probes are never limited.
	for i := 0; i < 10; i++ {
		if rec, _ := get(t, srv, "/v1/health", ""); rec.Code == http.StatusTooManyRequests {
			t.Fatalf("GET health should never be rate limited")
		}
	}
	// POSTs are limited after RateLimitRPM within the window.
	limited := false
	for i := 0; i < 6; i++ {
		req := httptest.NewRequest(http.MethodPost, "/v1/auth/login", jsonBody(`{"email":"a@b.io","password":"x"}`))
		rec := httptest.NewRecorder()
		srv.Handler().ServeHTTP(rec, req)
		if rec.Code == http.StatusTooManyRequests {
			limited = true
			break
		}
	}
	if !limited {
		t.Errorf("expected a 429 after exceeding the rate limit")
	}
}