package httpapi_test import ( "database/sql" "net/http" "testing" ) // 已发布成绩不能被「存草稿」就地覆盖:ON CONFLICT 不改 state, // 放行会把 rank 清空但流水不动,大屏奖牌与积分立刻对不上。 func TestSaveDraftRejectsPublishedResult(t *testing.T) { env := newTestEnv(t) ids := env.seed(t) env.createUser("recorder", "recorder-pass", "recorder") client, _ := env.client() env.login(client, "recorder", "recorder-pass") env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 11.58, "unit": "s", "status": "ok"}, {"entryId": ids.entryB, "resultValue": 11.92, "unit": "s", "status": "ok"}, }, }, true) if status, env2 := env.do(client, http.MethodPost, "/api/results/publish", map[string]any{ "eventId": ids.event, }, true); status != http.StatusOK { t.Fatalf("发布应 200,得到 %d %+v", status, env2.Error) } status, env2 := env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 12.00, "status": "ok"}, }, }, true) if status != http.StatusConflict || env2.Error == nil || env2.Error.Code != "STATE_INVALID" { t.Fatalf("已发布成绩存草稿应 409 STATE_INVALID,得到 %d %+v", status, env2.Error) } var ( rank int value float64 state string ) if err := env.ev.QueryRow(`SELECT rank, result_value, state FROM results WHERE entry_id = ?`, ids.entryA). Scan(&rank, &value, &state); err != nil { t.Fatalf("读取成绩: %v", err) } if rank != 1 || value != 11.58 || state != "published" { t.Fatalf("被拒的存草稿不得改动已发布行: rank=%d value=%v state=%s", rank, value, state) } // 正确流程:先撤销发布,再存草稿。 if status, env2 := env.do(client, http.MethodPost, "/api/results/unpublish", map[string]any{ "eventId": ids.event, }, true); status != http.StatusOK { t.Fatalf("撤销发布应 200,得到 %d %+v", status, env2.Error) } status, env2 = env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 12.00, "unit": "s", "status": "ok"}, }, }, true) if status != http.StatusOK || !env2.OK { t.Fatalf("撤销发布后存草稿应 200,得到 %d %+v", status, env2.Error) } } // 草稿未传 unit 时应保留原值,否则 100 米成绩会从 1:06.33 退化成 66.33。 func TestSaveDraftKeepsUnitWhenOmitted(t *testing.T) { env := newTestEnv(t) ids := env.seed(t) env.createUser("recorder", "recorder-pass", "recorder") client, _ := env.client() env.login(client, "recorder", "recorder-pass") env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 66.33, "unit": "s", "status": "ok"}, }, }, true) env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 66.33, "resultText": "1:06.33", "status": "ok"}, }, }, true) var unit sql.NullString if err := env.ev.QueryRow(`SELECT unit FROM results WHERE entry_id = ?`, ids.entryA).Scan(&unit); err != nil { t.Fatalf("读取单位: %v", err) } if !unit.Valid || unit.String != "s" { t.Fatalf("未传 unit 时应保留原值 s,得到 %+v", unit) } // 显式传 unit 仍可覆盖。 env.do(client, http.MethodPut, "/api/results/draft", map[string]any{ "eventId": ids.event, "results": []map[string]any{ {"entryId": ids.entryA, "resultValue": 1.1, "unit": "min", "status": "ok"}, }, }, true) if err := env.ev.QueryRow(`SELECT unit FROM results WHERE entry_id = ?`, ids.entryA).Scan(&unit); err != nil { t.Fatalf("读取单位: %v", err) } if !unit.Valid || unit.String != "min" { t.Fatalf("显式传 unit 应覆盖为 min,得到 %+v", unit) } }