- Server no longer mirrors app.Runtime services (gov/svc/pwSvc/...): 574 call sites now use accessors in runtime_accessors.go - backgroundLoop (mutex+sync.Once) stops sweeper/publisher/reminder/wsGC goroutines race-free (baseline44161d2areproduced DATA RACE; now green under -race) - goose 00071 duplicate (dialogue merge1da17a28brought two 00071): context-conflict-adjudications renumbered to 00072; dev DB applied only through 00070 so zero data migration - task_finalize tests build minimal app.Runtime; test injection via serverTestOverrides + setOverride* (explicit-nil markers for fail-closed cases)
133 lines
4.1 KiB
Go
133 lines
4.1 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"chat-agent-system/internal/auth"
|
|
"chat-agent-system/internal/authz"
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/db"
|
|
)
|
|
|
|
func pgConfigFromTestDSN(t *testing.T) (*config.Config, *pgxpool.Pool) {
|
|
t.Helper()
|
|
dsn := db.SkipIfNoTestDatabaseURL(t)
|
|
parsed, err := url.Parse(dsn)
|
|
if err != nil {
|
|
t.Fatalf("parse TEST_DATABASE_URL: %v", err)
|
|
}
|
|
host, portStr, splitErr := net.SplitHostPort(parsed.Host)
|
|
if splitErr != nil {
|
|
host = parsed.Host
|
|
portStr = "5432"
|
|
}
|
|
port, _ := strconv.Atoi(portStr)
|
|
user := parsed.User.Username()
|
|
password, _ := parsed.User.Password()
|
|
dbName := strings.TrimPrefix(parsed.Path, "/")
|
|
cfg := &config.Config{
|
|
Env: "test",
|
|
DataDir: t.TempDir(),
|
|
AgentBackend: "stub",
|
|
AllowInMemory: false,
|
|
ContextDbHost: host,
|
|
ContextDbPort: port,
|
|
ContextDbUser: user,
|
|
ContextDbPassword: password,
|
|
ContextDbName: dbName,
|
|
AdminUsername: envOrFF("ADMIN_USERNAME", "admin"),
|
|
AdminPassword: envOrFF("ADMIN_PASSWORD", "admin-secret-password-123"),
|
|
}
|
|
pool, err := pgxpool.New(context.Background(), dsn)
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable: %v", err)
|
|
}
|
|
if err := pool.Ping(context.Background()); err != nil {
|
|
pool.Close()
|
|
t.Skipf("postgres ping failed: %v", err)
|
|
}
|
|
return cfg, pool
|
|
}
|
|
|
|
func envOrFF(key, fallback string) string {
|
|
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// Non-member must receive 403 on POST /api/v1/freeform-documents (PG authorizer).
|
|
func TestCreateFreeformDoc_NonMemberForbidden_PG(t *testing.T) {
|
|
cfg, pool := pgConfigFromTestDSN(t)
|
|
defer pool.Close()
|
|
|
|
var projectID int64
|
|
if err := pool.QueryRow(context.Background(), `SELECT id FROM projects ORDER BY id ASC LIMIT 1`).Scan(&projectID); err != nil || projectID <= 0 {
|
|
t.Skip("no projects for non-member create test")
|
|
}
|
|
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := NewServer(cfg, svc)
|
|
srv.setOverrideAuthorizer(authz.NewAuthorizer(pool))
|
|
|
|
uname := fmt.Sprintf("outsider_ff_%d", time.Now().UnixNano())
|
|
pass := "OutsiderPass123!"
|
|
regBody, _ := json.Marshal(map[string]string{
|
|
"username": uname,
|
|
"password": pass,
|
|
"name": "局外人",
|
|
})
|
|
regReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewReader(regBody))
|
|
regW := httptest.NewRecorder()
|
|
srv.Routes().ServeHTTP(regW, regReq)
|
|
if regW.Code != http.StatusOK && regW.Code != http.StatusCreated {
|
|
t.Skipf("register outsider unavailable: %d %s", regW.Code, regW.Body.String())
|
|
}
|
|
|
|
loginBody, _ := json.Marshal(map[string]string{"username": uname, "password": pass})
|
|
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewReader(loginBody))
|
|
loginW := httptest.NewRecorder()
|
|
srv.Routes().ServeHTTP(loginW, loginReq)
|
|
if loginW.Code != http.StatusOK {
|
|
t.Fatalf("outsider login failed: %d %s", loginW.Code, loginW.Body.String())
|
|
}
|
|
var session auth.SessionInfo
|
|
if err := json.NewDecoder(loginW.Body).Decode(&session); err != nil || session.Token == "" || session.User == nil {
|
|
t.Fatalf("decode outsider session: %v body=%s", err, loginW.Body.String())
|
|
}
|
|
|
|
if err := srv.authorizer().RequireProjectAccess(context.Background(), projectID, session.User.ID); err != authz.ErrForbidden {
|
|
t.Skipf("registered user unexpectedly has access to project %d: %v", projectID, err)
|
|
}
|
|
|
|
createBody, _ := json.Marshal(map[string]any{
|
|
"project_id": projectID,
|
|
"title": "should-fail",
|
|
"content_markdown": "# x\n",
|
|
"space": "my_private",
|
|
"client_key": fmt.Sprintf("doc-upload-outsider-%d", time.Now().UnixNano()),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/freeform-documents", bytes.NewReader(createBody))
|
|
withBearerAuth(req, session.Token)
|
|
w := httptest.NewRecorder()
|
|
srv.Routes().ServeHTTP(w, req)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("non-member create want 403 got %d %s", w.Code, w.Body.String())
|
|
}
|
|
}
|