Files
tamabox/internal/db/restriction_test.go
2026-10-18 21:14:59 +08:00

155 lines
6.0 KiB
Go

// 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: "[email protected]", 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: "[email protected]", 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))
}