// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "testing" "time" "github.com/stretchr/testify/require" ) // TestRestrictions 覆盖站方处置的读写语义与「L1 停止分发」的曝光摘除。 // 这些行为没有管理后台兜底(处置只能经 CLI 写入),一旦语义写错无法从界面发现, // 因此用数据库测试把「替换 / 到期 / 撤销 / 曝光排除」四条钉死。 func TestRestrictions(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() usersStore := NewUsersStore(database) store := NewRestrictionsStore(database) t.Cleanup(func() { if err := cleanup("users", "user_restrictions"); err != nil { t.Fatal(err) } }) require.NoError(t, usersStore.Create(ctx, CreateUserOptions{ Name: "Box Owner", Password: "super_secret", Email: "r@example.com", Domain: "rbox", })) owner, err := usersStore.GetByDomain(ctx, "rbox") require.NoError(t, err) require.NoError(t, usersStore.Create(ctx, CreateUserOptions{ Name: "Other Owner", Password: "super_secret", Email: "o@example.com", Domain: "obox", })) other, err := usersStore.GetByDomain(ctx, "obox") require.NoError(t, err) // 两个箱主都上首页,用于验证处置后是否被摘除。 require.NoError(t, usersStore.UpdateTags(ctx, owner.ID, UpdateTagsOptions{Tags: "test", ShowInHome: true})) require.NoError(t, usersStore.UpdateTags(ctx, other.ID, UpdateTagsOptions{Tags: "test", ShowInHome: true})) // 无处置时:不排除任何人。 level, active, err := store.ActiveLevel(ctx, owner.ID) require.NoError(t, err) require.Equal(t, RestrictionLevel(""), level) require.Nil(t, active) list, _, err := usersStore.GetShowInHome(ctx, 1, 10, "latest") require.NoError(t, err) require.Len(t, list, 2) // 施加 L1。 created, err := store.Create(ctx, CreateRestrictionOptions{ UserID: owner.ID, Level: string(RestrictionL1), Reason: "组织挂人", CreatedBy: "test", Days: 7, }) require.NoError(t, err) require.WithinDuration(t, time.Now().AddDate(0, 0, 7), created.ExpiresAt, time.Minute) level, active, err = store.ActiveLevel(ctx, owner.ID) require.NoError(t, err) require.Equal(t, RestrictionL1, level) require.NotNil(t, active) require.Equal(t, "组织挂人", active.Reason) // L1 的核心语义:被处置箱主从首页 / 探索的展示池里摘除。 list, _, err = usersStore.GetShowInHome(ctx, 1, 10, "latest") require.NoError(t, err) require.Len(t, list, 1) require.Equal(t, other.ID, list[0].ID) // 批量版本只返回被处置者。 levels, err := store.ActiveLevels(ctx, []uint{owner.ID, other.ID}) require.NoError(t, err) require.Equal(t, map[uint]RestrictionLevel{owner.ID: RestrictionL1}, levels) // 替换语义:升级到 L3 后,旧记录保留但被撤销,同一时刻只有一条生效。 _, err = store.Create(ctx, CreateRestrictionOptions{ UserID: owner.ID, Level: string(RestrictionL3), Reason: "升级", CreatedBy: "test", Days: 3, }) require.NoError(t, err) level, _, err = store.ActiveLevel(ctx, owner.ID) require.NoError(t, err) require.Equal(t, RestrictionL3, level) history, err := store.ListByUserID(ctx, owner.ID) require.NoError(t, err) require.Len(t, history, 2) // ListByUserID 按创建时间倒序,第一条是新的 L3(未撤销),第二条是已撤销的 L1。 require.Equal(t, string(RestrictionL3), history[0].Level) require.Nil(t, history[0].RevokedAt) require.Equal(t, string(RestrictionL1), history[1].Level) require.NotNil(t, history[1].RevokedAt) // 到期即失效:直接把到期时间改到过去,ActiveLevel 必须返回空(处置自动解除,不需要定时任务)。 require.NoError(t, database.Model(&UserRestriction{}). Where("user_id = ? AND revoked_at IS NULL", owner.ID). Update("expires_at", time.Now().Add(-time.Minute)).Error) level, active, err = store.ActiveLevel(ctx, owner.ID) require.NoError(t, err) require.Equal(t, RestrictionLevel(""), level) require.Nil(t, active) // 过期后曝光自动恢复。 list, _, err = usersStore.GetShowInHome(ctx, 1, 10, "latest") require.NoError(t, err) require.Len(t, list, 2) // 撤销语义:有效期内撤销后立即恢复可见。 _, err = store.Create(ctx, CreateRestrictionOptions{ UserID: owner.ID, Level: string(RestrictionL1), Reason: "再处置", CreatedBy: "test", Days: 7, }) require.NoError(t, err) affected, err := store.RevokeActive(ctx, owner.ID) require.NoError(t, err) require.EqualValues(t, 1, affected) list, _, err = usersStore.GetShowInHome(ctx, 1, 10, "latest") require.NoError(t, err) require.Len(t, list, 2) // 幂等:再次撤销不再有任何生效记录。 affected, err = store.RevokeActive(ctx, owner.ID) require.NoError(t, err) require.EqualValues(t, 0, affected) } // TestRestrictionLevelValidation 防止脏级别进入 ORDER BY level DESC 的排序("L9" 会排在 "L4" 之后)。 func TestRestrictionLevelValidation(t *testing.T) { require.True(t, IsValidRestrictionLevel(string(RestrictionL1))) require.True(t, IsValidRestrictionLevel(string(RestrictionL4))) require.False(t, IsValidRestrictionLevel("L5")) require.False(t, IsValidRestrictionLevel("l1")) // 大小写敏感,避免 CLI 误传 require.False(t, IsValidRestrictionLevel("")) } // TestRestrictionAtLeast 守住发布侧拦截的判定:L2 收窄公开、L3 冻结发布都依赖这个比较。 // 空级别(无处置)与未知级别必须恒为 false,否则脏数据会把正常箱主锁死。 func TestRestrictionAtLeast(t *testing.T) { require.True(t, RestrictionAtLeast(RestrictionL2, RestrictionL1)) require.True(t, RestrictionAtLeast(RestrictionL4, RestrictionL3)) require.True(t, RestrictionAtLeast(RestrictionL2, RestrictionL2)) require.False(t, RestrictionAtLeast(RestrictionL1, RestrictionL2)) require.False(t, RestrictionAtLeast(RestrictionL3, RestrictionL4)) require.False(t, RestrictionAtLeast("", RestrictionL1)) require.False(t, RestrictionAtLeast("L9", RestrictionL1)) }