568329890b
上一版 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] 这类二维下标形态。
146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|