feat: 基础框架搭建完毕
Checks / verify (push) Failing after 2m37s

This commit is contained in:
2026-10-07 09:27:47 +08:00
parent ba6dfffab7
commit bffdb3858b
64 changed files with 326 additions and 332 deletions
@@ -0,0 +1,181 @@
package integration
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
"git.sundynix.cn/Blizzard/Veyra/internal/database"
"git.sundynix.cn/Blizzard/Veyra/internal/handler"
"git.sundynix.cn/Blizzard/Veyra/internal/repository"
"git.sundynix.cn/Blizzard/Veyra/internal/service"
)
func TestIdentityAccountLifecycle(t *testing.T) {
dsn := os.Getenv("VEYRA_TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set VEYRA_TEST_DATABASE_URL for real database integration")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
admin, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer admin.Close()
schema := "veyra_test_" + strings.ReplaceAll(uuid.NewString(), "-", "")
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
t.Fatal(err)
}
defer func() {
if _, err := admin.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE"); err != nil {
t.Error(err)
}
}()
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
t.Fatal(err)
}
cfg.ConnConfig.RuntimeParams["search_path"] = schema
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
if err := database.Migrate(ctx, pool); err != nil {
t.Fatal(err)
}
auth, err := service.NewAuthService(repository.NewIdentityRepository(pool))
if err != nil {
t.Fatal(err)
}
u1, err := auth.ProvisionUser(ctx, "alice", "a-test-only-password", "Alice", "USER")
if err != nil {
t.Fatal(err)
}
u2, err := auth.ProvisionUser(ctx, "bob", "another-test-password", "Bob", "USER")
if err != nil {
t.Fatal(err)
}
if _, err := auth.ProvisionUser(ctx, "alice", "a-test-only-password", "Duplicate", "USER"); !errors.Is(err, apperror.ErrConflict) {
t.Fatalf("duplicate identity: %v", err)
}
var count int
if err := pool.QueryRow(ctx, "SELECT count(*) FROM veyra_user").Scan(&count); err != nil || count != 2 {
t.Fatal("credential conflict left an orphan user")
}
login, err := auth.Login(ctx, "alice", "a-test-only-password", "app")
if err != nil {
t.Fatal(err)
}
if _, err := auth.Authenticate(ctx, login.Token, "admin"); !errors.Is(err, apperror.ErrUnauthenticated) {
t.Fatal("app session escaped audience")
}
if _, err := auth.Authenticate(ctx, login.Token, "app"); err != nil {
t.Fatal(err)
}
if err := auth.Logout(ctx, login.Token, "app"); err != nil {
t.Fatal(err)
}
if _, err := auth.Authenticate(ctx, login.Token, "app"); !errors.Is(err, apperror.ErrUnauthenticated) {
t.Fatal("revoked session remained valid")
}
accounts := service.NewAccountService(repository.NewAccountRepository(pool))
cash := "100000.01"
a, err := accounts.Create(ctx, u1.ID, "simulation", "Simulation", &cash)
if err != nil || a.CashBalance == nil || *a.CashBalance != cash {
t.Fatalf("simulation balance: %v", err)
}
if _, err := accounts.Get(ctx, u2.ID, a.ID); !errors.Is(err, apperror.ErrNotFound) {
t.Fatal("cross-user account disclosed")
}
if _, err := accounts.Create(ctx, u1.ID, "simulation", "Duplicate", &cash); !errors.Is(err, apperror.ErrConflict) {
t.Fatal("duplicate simulation account accepted")
}
if err := pool.QueryRow(ctx, "SELECT count(*) FROM veyra_cash_entry").Scan(&count); err != nil || count != 1 {
t.Fatal("duplicate creation left a cash entry")
}
real, err := accounts.Create(ctx, u1.ID, "manual_real", "Manual", nil)
if err != nil || real.CashBalance != nil || real.LedgerStatus != "INCOMPLETE" {
t.Fatalf("manual account fabricated cash: %v", err)
}
if _, err := accounts.Create(ctx, u2.ID, "manual_real", "Manual", &cash); !errors.Is(err, apperror.ErrInvalidInput) {
t.Fatal("manual account accepted simulated funding")
}
list, err := accounts.List(ctx, u2.ID)
if err != nil || len(list) != 0 {
t.Fatal("account list escaped owner scope")
}
login, err = auth.Login(ctx, "alice", "a-test-only-password", "app")
if err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, "UPDATE veyra_session SET created_at=now()-interval '1 day',expires_at=now()-interval '1 hour' WHERE token_hash=$1", service.TokenHash(login.Token)); err != nil {
t.Fatal(err)
}
if _, err := auth.Authenticate(ctx, login.Token, "app"); !errors.Is(err, apperror.ErrUnauthenticated) {
t.Fatal("expired session remained valid")
}
if _, err := auth.ProvisionUser(ctx, "admin", "an-admin-test-password", "Admin", "ADMIN"); err != nil {
t.Fatal(err)
}
probe := func(context.Context) error { return nil }
router := handler.NewRouter(service.NewHealthService(probe, probe), slog.New(slog.NewTextHandler(io.Discard, nil)), handler.IdentityOptions{Auth: auth, Accounts: accounts, Limiter: allowLogin{}, DevelopmentLogin: true})
call := func(method, path, body, origin string, cookie *http.Cookie) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, "http://example.com"+path, strings.NewReader(body))
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
if origin != "" {
req.Header.Set("Origin", origin)
}
if cookie != nil {
req.AddCookie(cookie)
}
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec
}
response := call(http.MethodPost, "/api/admin/v1/auth/login", `{"login_name":"admin","password":"an-admin-test-password"}`, "http://example.com", nil)
if response.Code != http.StatusOK {
t.Fatalf("admin login failed: %d", response.Code)
}
cookies := response.Result().Cookies()
if len(cookies) != 1 || !cookies[0].HttpOnly || cookies[0].SameSite != http.SameSiteStrictMode || cookies[0].Path != "/api/admin" {
t.Fatal("admin cookie constraints missing")
}
cookie := cookies[0]
if strings.Contains(response.Body.String(), cookie.Value) {
t.Fatal("admin token leaked into response body")
}
if response := call(http.MethodGet, "/api/admin/v1/me", "", "", cookie); response.Code != http.StatusOK {
t.Fatal("cookie authentication failed")
}
if response := call(http.MethodPost, "/api/admin/v1/auth/logout", "", "http://attacker.example", cookie); response.Code != http.StatusForbidden {
t.Fatal("cross-origin logout allowed")
}
if response := call(http.MethodGet, "/api/admin/v1/me", "", "", cookie); response.Code != http.StatusOK {
t.Fatal("CSRF attempt revoked a session")
}
if response := call(http.MethodPost, "/api/admin/v1/auth/logout", "", "http://example.com", cookie); response.Code != http.StatusOK {
t.Fatal("logout failed")
}
if response := call(http.MethodGet, "/api/admin/v1/me", "", "", cookie); response.Code != http.StatusUnauthorized {
t.Fatal("revoked admin cookie remained valid")
}
}
type allowLogin struct{}
func (allowLogin) Login(context.Context, string, string) (bool, error) { return true, nil }