feat: complete remaining Go backend modules
This commit is contained in:
1 parent
cea2751dc5
commit
aef5a97165
145 files changed
+18376
-199
No files matched your search
@@ -2,13 +2,99 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/administration"
|
||||
)
|
||||
|
||||
func TestApplyAccountUpdateUsesNarrowAtomicSQLAndDatabaseVersionIncrement(t *testing.T) {
|
||||
now := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC)
|
||||
rows := &administrationRows{rows: [][]any{{
|
||||
"user-1", "13800138000", "New Name", "user", "org-1", "active",
|
||||
"new-password-hash", "new-password-salt", 3, now.Add(time.Hour), 9, now.Add(-time.Hour),
|
||||
nil, now.Add(-24 * time.Hour), now,
|
||||
}}}
|
||||
querier := &administrationQuerier{rows: rows}
|
||||
db := NewDatabase(Config{Backend: BackendPostgres}, querier)
|
||||
name := "New Name"
|
||||
|
||||
got, err := db.ApplyAccountUpdate(context.Background(), "user-1", administration.AccountUpdate{
|
||||
Actor: administration.Actor{ID: "super", Role: administration.RoleSuperAdmin},
|
||||
DisplayName: &name,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.SessionVersion != 9 || got.PasswordHash != "new-password-hash" || got.FailedLoginCount != 3 {
|
||||
t.Fatalf("updated account=%#v, want database-owned security fields preserved", got)
|
||||
}
|
||||
if querier.sql != ApplyAdministrationAccountUpdateSQL {
|
||||
t.Fatalf("SQL=%q", querier.sql)
|
||||
}
|
||||
for _, forbidden := range []string{"phone=$", "created_at=$", "last_login_at=$", "legacy_subject=$"} {
|
||||
if strings.Contains(querier.sql, forbidden) {
|
||||
t.Fatalf("atomic PATCH must not overwrite unrelated column: SQL contains %q", forbidden)
|
||||
}
|
||||
}
|
||||
for _, required := range []string{
|
||||
"password_hash = CASE WHEN $10::boolean THEN $11::text ELSE password_hash END",
|
||||
"failed_login_count = CASE WHEN $13::boolean THEN 0 ELSE failed_login_count END",
|
||||
"session_version = session_version + CASE WHEN $14::boolean THEN 1 ELSE 0 END",
|
||||
} {
|
||||
if !strings.Contains(querier.sql, required) {
|
||||
t.Fatalf("atomic PATCH SQL missing %q", required)
|
||||
}
|
||||
}
|
||||
wantArgs := []any{
|
||||
"user-1",
|
||||
true, "New Name",
|
||||
false, administration.Role(""),
|
||||
false, nil,
|
||||
false, administration.Status(""),
|
||||
false, "", "",
|
||||
false, false,
|
||||
now,
|
||||
administration.RoleSuperAdmin, "",
|
||||
}
|
||||
if !reflect.DeepEqual(querier.args, wantArgs) {
|
||||
t.Fatalf("args=%#v want %#v", querier.args, wantArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAccountUpdateAtomicallyResetsPasswordClearsLockAndIncrementsCurrentVersion(t *testing.T) {
|
||||
now := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC)
|
||||
rows := &administrationRows{rows: [][]any{{
|
||||
"user-1", "13800138000", "Name", "user", "org-1", "active",
|
||||
"replacement-hash", "replacement-salt", 0, nil, 12, nil,
|
||||
nil, now.Add(-24 * time.Hour), now,
|
||||
}}}
|
||||
querier := &administrationQuerier{rows: rows}
|
||||
db := NewDatabase(Config{Backend: BackendPostgres}, querier)
|
||||
|
||||
got, err := db.ApplyAccountUpdate(context.Background(), "user-1", administration.AccountUpdate{
|
||||
Actor: administration.Actor{Role: administration.RoleOrganizationAdmin, OrganizationID: "org-1"},
|
||||
PasswordHash: &administration.PasswordHash{Hash: "replacement-hash", Salt: "replacement-salt"},
|
||||
ClearLoginLock: true,
|
||||
IncrementSessionVersion: true,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.SessionVersion != 12 || got.FailedLoginCount != 0 || got.LockedUntil != nil {
|
||||
t.Fatalf("updated account=%#v", got)
|
||||
}
|
||||
if !reflect.DeepEqual(querier.args[9:14], []any{true, "replacement-hash", "replacement-salt", true, true}) {
|
||||
t.Fatalf("security args=%#v", querier.args[9:14])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdministrationListAccountsUsesExplicitColumnsAndParameterizedFilters(t *testing.T) {
|
||||
rows := &identityRows{}
|
||||
querier := &identityQuerier{rows: rows}
|
||||
@@ -31,7 +117,7 @@ func TestDeleteAccountArchivesAllOwnedDataAndIdentityInOneTransaction(t *testing
|
||||
if err := db.DeleteAccount(context.Background(), "user-1", "archive:org-1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantSQL := []string{ArchiveAssetsSQL, ArchiveGenerationJobsSQL, ArchiveProjectsSQL, ArchiveImageTemplatesSQL, DeleteAdministrationAccountSQL}
|
||||
wantSQL := []string{ArchiveAssetsSQL, ArchiveGenerationJobsSQL, ArchiveProjectsSQL, ArchiveImageTemplatesSQL, ArchiveUsageEventsSQL, DeleteAdministrationAccountSQL}
|
||||
if !reflect.DeepEqual(tx.sql, wantSQL) {
|
||||
t.Fatalf("SQL sequence=%#v want %#v", tx.sql, wantSQL)
|
||||
}
|
||||
@@ -94,3 +180,56 @@ func (t *administrationTransaction) Exec(_ context.Context, query string, args .
|
||||
}
|
||||
func (t *administrationTransaction) Commit(context.Context) error { t.commits++; return nil }
|
||||
func (t *administrationTransaction) Rollback(context.Context) error { t.rollbacks++; return nil }
|
||||
|
||||
type administrationQuerier struct {
|
||||
rows *administrationRows
|
||||
sql string
|
||||
args []any
|
||||
}
|
||||
|
||||
func (q *administrationQuerier) Query(_ context.Context, query string, args ...any) (Rows, error) {
|
||||
q.sql, q.args = query, args
|
||||
return q.rows, nil
|
||||
}
|
||||
|
||||
type administrationRows struct {
|
||||
rows [][]any
|
||||
idx int
|
||||
}
|
||||
|
||||
func (r *administrationRows) Close() {}
|
||||
func (r *administrationRows) Err() error { return nil }
|
||||
func (r *administrationRows) Next() bool { return r.idx < len(r.rows) }
|
||||
func (r *administrationRows) Scan(dest ...any) error {
|
||||
if r.idx >= len(r.rows) || len(dest) != len(r.rows[r.idx]) {
|
||||
return errors.New("invalid administration row scan")
|
||||
}
|
||||
row := r.rows[r.idx]
|
||||
r.idx++
|
||||
for i, target := range dest {
|
||||
value := row[i]
|
||||
switch target := target.(type) {
|
||||
case *string:
|
||||
*target = value.(string)
|
||||
case *int:
|
||||
*target = value.(int)
|
||||
case *administration.Role:
|
||||
*target = administration.Role(value.(string))
|
||||
case *administration.Status:
|
||||
*target = administration.Status(value.(string))
|
||||
case *time.Time:
|
||||
*target = value.(time.Time)
|
||||
case *sql.NullString:
|
||||
if value != nil {
|
||||
*target = sql.NullString{String: value.(string), Valid: true}
|
||||
}
|
||||
case *sql.NullTime:
|
||||
if value != nil {
|
||||
*target = sql.NullTime{Time: value.(time.Time), Valid: true}
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported administration scan target")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user