Files
rond-with-you/internal/web/topics_dataset_test.go
T
tamakyi 770e752a7a 图片恢复支持对象存储(R2);专题也改成归人不归数据集
R2 上的图怎么救:库里的行没了就只剩桶里有那些键,不列桶无从得知它们叫什么。
给 internal/media 加了 ListObjects(ListObjectsV2 + SigV4 + 自动翻页 + XML 解析):
- 路径式访问、请求只到桶;查询串必须**先百分号编码再签名**(canonicalQuery 按原样取
  RawQuery,两边得是同一份字节,prefix 里的 / 要写成 %2F)。
- ⚠️ R2 的 API Token 默认只给对象读写,没有 s3:ListBucket 就 403。列桶失败**不让整个
  恢复失败**:本地那半照救,原因进 Warnings、页面用红色提示条显示(否则站长会以为恢复完了)。
- 候选对象 = 本地目录 ∪ 桶里 .webp(同键以本地为准);行的 storage 按「对象实际在哪」写,
  bytes 用桶里报的 Size。另加一条 warning:挂回了却没配 s3_public_base ⇒ 行有了页面空白,
  这比「没恢复」更难懂。

专题(用户拍板「一并做」):topics.dataset_id 同样带 CASCADE,删数据集带走专题,而专题
只存在这张表里、没有恢复来源。外键摘掉,所有查询 dataset_id → user_id(loadTopics、
topicPage、保存/删除/开关、pageBase.HasTopics);**内容仍照当前生效数据集算**
(topicContent 不动)——变的是「这份定义归谁」。Dataset 补 UserID 供前台按属主定位
用户级数据;管理专题不再要求有生效数据集。

导出链路未改动:place_images / topics / Dataset.UserID 都不参与 .rondbackup 导出,
全量验收(含导出保真、ZRAW 夹取、编号对齐那批)PASS=331 FAIL=0 SKIP=0。
2026-09-30 23:31:17 +08:00

92 lines
3.4 KiB
Go

package web
import (
"testing"
)
// ---------- 专题:归人不归数据集 ----------
//
// 专题是「一段时间 + 一块区域」的定义,不是备份里的数据:换数据集之后它照旧成立
// (内容照当前生效数据集算),删掉旧数据集不该把它带走。
// 早期版本 `topics.dataset_id` 带 ON DELETE CASCADE,实测丢过一批专题,而且**只存在这张表里**,
// 没有别的恢复来源。这两条用例守的就是「删数据集之后专题还在、还列得出来」。
// insertTopic 直接插一条专题(与页面同一张表,省去构造表单)。
func insertTopic(t *testing.T, f *movementFixture, title string) int64 {
t.Helper()
uid := userFrom(f.ctx).ID
var id int64
if err := f.pg.QueryRowContext(f.ctx, `INSERT INTO topics
(user_id, dataset_id, title, subtitle, description, start_at, end_at,
center_lat, center_lon, radius_km, show_fog, enabled, sort_order)
VALUES ($1,$2,$3,'','',now() - interval '30 days', now(), 0,0,0, true, true, 0)
RETURNING id`, uid, f.dsID, title).Scan(&id); err != nil {
t.Fatalf("插专题失败: %v", err)
}
return id
}
func topicIDs(t *testing.T, f *movementFixture, uid int64) []int64 {
t.Helper()
list, err := f.srv.loadTopics(f.ctx, uid, false)
if err != nil {
t.Fatalf("读专题失败: %v", err)
}
var out []int64
for _, tp := range list {
out = append(out, tp.ID)
}
return out
}
// TestTopicsSurviveDatasetDelete 删数据集不再带走专题,删完还能按属主列出来。
func TestTopicsSurviveDatasetDelete(t *testing.T) {
f := newMovementFixture(t)
uid := userFrom(f.ctx).ID
want := insertTopic(t, f, "2026 国庆")
if got := topicIDs(t, f, uid); len(got) != 1 || got[0] != want {
t.Fatalf("建完之后应当能按属主列出这一条,实际 %v", got)
}
// 外键已摘:删数据集不级联到专题
if _, err := f.pg.ExecContext(f.ctx, `DELETE FROM datasets WHERE id=$1`, f.dsID); err != nil {
t.Fatalf("删数据集失败: %v", err)
}
var n int
if err := f.pg.QueryRowContext(f.ctx, `SELECT count(*) FROM topics WHERE id=$1`, want).Scan(&n); err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("删数据集把专题带走了(外键没摘干净?)")
}
if got := topicIDs(t, f, uid); len(got) != 1 || got[0] != want {
t.Errorf("没有生效数据集时也该列得出来(专题归人),实际 %v", got)
}
}
// TestTopicsForeignKeyDropped 直接盯住迁移本身:外键约束必须不在。
// 只看「删数据集没删专题」有可能被别的原因弄绿(比如根本没建数据集约束),
// 这里把判据钉在 `pg_constraint` 上。
func TestTopicsForeignKeyDropped(t *testing.T) {
f := newMovementFixture(t)
var del string
err := f.pg.QueryRowContext(f.ctx, `SELECT confdeltype FROM pg_constraint
WHERE conname='topics_dataset_id_fkey'`).Scan(&del)
if err == nil {
t.Errorf("topics 上还有 datasets 的外键(删除行为=%s):删数据集会把专题一起带走", del)
}
// 专题只该跟着**用户**走:删用户时它必须一起没,不能留下孤儿
uid := userFrom(f.ctx).ID
insertTopic(t, f, "会被用户带走")
if _, err := f.pg.ExecContext(f.ctx, `DELETE FROM users WHERE id=$1`, uid); err != nil {
t.Fatalf("删用户失败: %v", err)
}
var left int
if err := f.pg.QueryRowContext(f.ctx, `SELECT count(*) FROM topics WHERE user_id=$1`, uid).Scan(&left); err != nil {
t.Fatal(err)
}
if left != 0 {
t.Errorf("删用户后专题还留着 %d 条", left)
}
}