Files
cpdaily_xiaozhushou/backed/internal/service/cpdaily/leave_xsgz_test.go
T
becomer 568329890b 回退 f1f79df:抓取线上前端源码证实数组下标确为方括号 fields[0]
上一版 f1f79df 把 xsgzFlatten 的数组下标从 fields[0] 改成 fields.0,
依据是函数注释/提交说明/「needCurlyBraces 语义」三处都写点号。
本次从 sdau 线上静态资源(无需登录)拿到真实前端源码,证明该判断错误。

证据(xsgz.sdau.edu.cn/assets/,免登录可取):
  index-B6fhXjS6.js      10.7MB 主包,含 1354 个 chunk 清单
  studentForm-Dx5Fpjfs.js       序列化与提交实现
  LeaveApplyDrawer-BeaVSGRQ.js  请假提交调用点

前端真实链路(LeaveApplyDrawer 的 P()):
  c = ee(表单值, 字段定义)
  L = ae(c, 字段定义)                      // fields 数组
  T = {projectId, requestType:"LEAVE", startTime, endTime,
       fields:L, address, longitude, latitude}
  f = le(T, ext)                          // le → Ne → v → pe,转 FormData
  await te(f)                             // POST application_submit (multipart)

键名规则(studentForm-Dx5Fpjfs.js 原文):
  function pe(e,t,a){return e? a? `${e}[${t}]` : `${e}.${t}` : t}
  v(): if(Array.isArray(s)) s.forEach((l,p)=>{
         const c=`${i}[${p}]`;            // 数组下标永远方括号,与 a 无关
         ... v(l,r,c,o)
       })
  Ne(e, t={}, a=false) → v(..., a=false)

即:对象键用点号,数组下标硬编码方括号。实际提交形如
fields[0].fieldId / fields[0].value。原实现正确,f1f79df 改错了。

另外查明:注释里长期引用的 toFormatFormData / needCurlyBraces
在整个 bundle 中并不存在(grep 计数为 0),是早期臆造的符号,
现已在注释中改为真实函数名 v()/pe()/Ne() 并附调用链。

改动:
- xsgzFlatten 数组分支还原为 "%s[%d]",注释给出源码依据;
- XsgzSubmitApplication 注释改为真实前端链路与规则;
- TestXsgzFlatten 期望还原为 fields[0].*;
- 契约测试期望同步为 fields[0].*,并加断言:括号需配对、
  不得出现 fields[0][fieldId] 这类二维下标形态。
2026-10-17 18:15:35 +08:00

146 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cpdaily
import "testing"
// 参考向量由真实前端 security.js(RSAUtils.encryptedString)在 Node 中生成,
// 用于确保 Go 实现与浏览器行为逐字节一致。
// 注意:模数 M 是学校固定的;DK 盐每次 /api/sec_js 都变,此处为取样时的一次值。
const (
vecXsgzDK = "54b6fay56a10ef75coxxjxl4yryye31u"
vecXsgzE = "010001"
vecXsgzM = "00b36e050adb7347cacb80ea0f7831ff64cdf8569adfb5a19099e95870f4b6aab43f75a221b1090e3a9ed91b420c72a6e38dc3b5594ddc30908db5266d5bf4474586080d0eb425bbc185185eec64c4596e6113d08fba77337b4171d91daad26b4571f1108ddf687fca25d8d5a75e3ec5dd698dff8446329c78e63d170eb401da55"
vecXsgzTicket = "ST-UNIT-TEST-0001"
vecXsgzWant = "3651cc9f29c5ff7522f648c0bd2fc671bb2009e82b7860bc2714238dbaf55a90a811dcc0d736d5e6185c084720b72a17b059d5dd2558ce44ed8a2f39aa60c85212acd8c6778377e4e80fb631a5a5259e557cf44750cf500bf0100860d0afb0027d8f1a93f5d725703a54bc3b0412c7e9e75a1bf4b4abce044a91d5acaadd0d19"
)
// TestXsgzEncryptTicket 校验 RSA 加密与前端 RSAUtils 完全一致。
func TestXsgzEncryptTicket(t *testing.T) {
r := &xsgzRSA{DK: vecXsgzDK, M: vecXsgzM, E: vecXsgzE}
got, err := xsgzEncryptTicket(vecXsgzTicket, r)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
if got != vecXsgzWant {
t.Fatalf("密文与前端不一致:\n got=%s\nwant=%s", got, vecXsgzWant)
}
}
// TestXsgzEncodeURIComponent 校验与 JS encodeURIComponent 等价(空格必须编 %20 而非 +)。
func TestXsgzEncodeURIComponent(t *testing.T) {
cases := []struct{ in, want string }{
{"abcXYZ019", "abcXYZ019"},
{"-_.!~*'()", "-_.!~*'()"},
{"a b", "a%20b"}, // 关键:不能是 a+b
{"a+b", "a%2Bb"}, // + 必须被编码
{"ST-123", "ST-123"}, // ticket 常见字符
{"中文", "%E4%B8%AD%E6%96%87"}, // UTF-8 逐字节
}
for _, c := range cases {
if got := xsgzEncodeURIComponent(c.in); got != c.want {
t.Fatalf("encodeURIComponent(%q)=%q, want %q", c.in, got, c.want)
}
}
}
// TestXsgzEncryptChunkSize 校验分块长度 = 模数字节数-2。
func TestXsgzEncryptChunkSize(t *testing.T) {
r := &xsgzRSA{DK: vecXsgzDK, M: vecXsgzM, E: vecXsgzE}
// 构造足够长的 ticket 触发多块
long := vecXsgzTicket
for i := 0; i < 10; i++ {
long += "0123456789"
}
got, err := xsgzEncryptTicket(long, r)
if err != nil {
t.Fatalf("加密失败: %v", err)
}
blocks := splitSpace(got)
if len(blocks) < 2 {
t.Fatalf("长输入应产生多块,实际 %d 块", len(blocks))
}
for _, b := range blocks {
if len(b) > 256 {
t.Fatalf("块长度 %d 超过模数 hex 长度 256", len(b))
}
}
}
func splitSpace(s string) []string {
var out []string
cur := ""
for _, c := range s {
if c == ' ' {
out = append(out, cur)
cur = ""
continue
}
cur += string(c)
}
if cur != "" {
out = append(out, cur)
}
return out
}
// TestIsXsgzSchool 校验按学校 ID 分流。
func TestIsXsgzSchool(t *testing.T) {
if !IsXsgzSchool("sdau") {
t.Fatal("sdau 应走新学工5")
}
if IsXsgzSchool("ynu") {
t.Fatal("普通学校不应走新学工5")
}
if xsgzBaseURL("sdau") == "" {
t.Fatal("sdau 新学工地址未配置")
}
if XsgzCasServiceURL("sdau") != "https://xsgz.sdau.edu.cn/oauth/callback" {
t.Fatalf("CAS service 地址错误: %s", XsgzCasServiceURL("sdau"))
}
}
// xsgzFlatten 必须与前端 studentForm-Dx5Fpjfs.js 的 v()/pe() 规则一致:
// 对象键用点号、数组下标用方括号、跳过空值。
func TestXsgzFlatten(t *testing.T) {
in := map[string]interface{}{
"projectId": "uuid-1",
"requestType": "CANCELLATION",
"address": "", // 空字符串应跳过
"longitude": "115.8",
"inArea": true,
"empty": nil, // nil 应跳过
"fields": []interface{}{
map[string]interface{}{"fieldId": "f1", "value": "v1", "blank": ""},
map[string]interface{}{"fieldId": "f2", "value": "v2"},
},
}
var pairs [][2]string
xsgzFlatten("", in, &pairs)
got := map[string]string{}
for _, kv := range pairs {
got[kv[0]] = kv[1]
}
want := map[string]string{
"projectId": "uuid-1",
"requestType": "CANCELLATION",
"longitude": "115.8",
"inArea": "true",
"fields[0].fieldId": "f1",
"fields[0].value": "v1",
"fields[1].fieldId": "f2",
"fields[1].value": "v2",
}
for k, v := range want {
if got[k] != v {
t.Fatalf("key %q = %q, want %q(实际: %v)", k, got[k], v, got)
}
}
// 空值不应出现
for _, bad := range []string{"address", "empty", "fields[0].blank"} {
if _, ok := got[bad]; ok {
t.Fatalf("空值键 %q 不应被提交", bad)
}
}
}