bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
357 lines
12 KiB
Go
357 lines
12 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apps
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestMaskAPIKey(t *testing.T) {
|
|
cases := map[string]string{
|
|
"": "****",
|
|
"abcd": "****",
|
|
"xxxxxxxxxxxx": "****xxxx",
|
|
}
|
|
for in, want := range cases {
|
|
if got := maskAPIKey(in); got != want {
|
|
t.Errorf("maskAPIKey(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactKeyInfo_StripsRawKey(t *testing.T) {
|
|
in := map[string]interface{}{
|
|
"api_key_id": "k1",
|
|
"api_key": "xxxxxxxxxxxx",
|
|
"name": "partner-test",
|
|
"status": float64(1),
|
|
}
|
|
out := redactKeyInfo(in)
|
|
if _, ok := out["api_key"]; ok {
|
|
t.Fatalf("redactKeyInfo must strip api_key, got %v", out)
|
|
}
|
|
if out["key_preview"] != "****xxxx" {
|
|
t.Errorf("key_preview = %v, want ****xxxx", out["key_preview"])
|
|
}
|
|
if out["name"] != "partner-test" || out["api_key_id"] != "k1" {
|
|
t.Errorf("non-secret fields must be preserved, got %v", out)
|
|
}
|
|
// input not mutated
|
|
if _, ok := in["api_key"]; !ok {
|
|
t.Errorf("redactKeyInfo must not mutate input")
|
|
}
|
|
}
|
|
|
|
func TestParseScopeAPI(t *testing.T) {
|
|
t.Run("valid", func(t *testing.T) {
|
|
info, err := parseScopeAPI("GET /openapi/v1/orders")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if info["http_method"] != "GET" {
|
|
t.Errorf("http_method = %v, want GET", info["http_method"])
|
|
}
|
|
if info["http_path"] != "/openapi/v1/orders" {
|
|
t.Errorf("http_path = %v, want /openapi/v1/orders", info["http_path"])
|
|
}
|
|
})
|
|
t.Run("lowercase method uppercased", func(t *testing.T) {
|
|
info, err := parseScopeAPI("post /openapi/x")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if info["http_method"] != "POST" {
|
|
t.Errorf("http_method = %v, want POST", info["http_method"])
|
|
}
|
|
})
|
|
t.Run("too few fields", func(t *testing.T) {
|
|
if _, err := parseScopeAPI("GET"); err == nil {
|
|
t.Errorf("one-word input must error")
|
|
}
|
|
})
|
|
t.Run("too many fields", func(t *testing.T) {
|
|
if _, err := parseScopeAPI("GET /openapi/x extra"); err == nil {
|
|
t.Errorf("three-word input must error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateScopeAPIMethod(t *testing.T) {
|
|
for _, m := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
|
|
if err := validateScopeAPIMethod(m); err != nil {
|
|
t.Errorf("validateScopeAPIMethod(%q) = %v, want nil", m, err)
|
|
}
|
|
}
|
|
for _, m := range []string{"TRACE", "CONNECT", "OPTIONS", "HEAD", "", "get"} {
|
|
if err := validateScopeAPIMethod(m); err == nil {
|
|
t.Errorf("validateScopeAPIMethod(%q) = nil, want error", m)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateScopeAPIPath(t *testing.T) {
|
|
for _, p := range []string{"/openapi/orders", "/openapi/v1/x"} {
|
|
if err := validateScopeAPIPath(p); err != nil {
|
|
t.Errorf("validateScopeAPIPath(%q) = %v, want nil", p, err)
|
|
}
|
|
}
|
|
for _, p := range []string{"", "openapi/x", "/openapi/../admin", "/..", "/openapi//x", "//x"} {
|
|
if err := validateScopeAPIPath(p); err == nil {
|
|
t.Errorf("validateScopeAPIPath(%q) = nil, want error", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateRequestScopeFields(t *testing.T) {
|
|
ok := []map[string]interface{}{
|
|
{"allow_all": true},
|
|
{"allow_all": false, "http_infos": []interface{}{
|
|
map[string]interface{}{"http_method": "GET", "http_path": "/openapi/x"},
|
|
}},
|
|
{},
|
|
}
|
|
for _, rs := range ok {
|
|
if err := validateRequestScopeFields(rs); err != nil {
|
|
t.Errorf("validateRequestScopeFields(%v) = %v, want nil", rs, err)
|
|
}
|
|
}
|
|
bad := []map[string]interface{}{
|
|
{"foo": 1}, // unknown top-level field
|
|
{"allow_all": "yes"}, // wrong type
|
|
{"http_infos": "x"}, // not an array
|
|
{"http_infos": []interface{}{"x"}}, // entry not an object
|
|
{"http_infos": []interface{}{map[string]interface{}{"http_method": "TRACE", "http_path": "/x"}}}, // bad method
|
|
{"http_infos": []interface{}{map[string]interface{}{"http_method": "GET", "http_path": "../x"}}}, // bad path
|
|
{"http_infos": []interface{}{map[string]interface{}{"http_method": "GET", "http_path": "/x", "extra": 1}}}, // unknown entry field
|
|
}
|
|
for _, rs := range bad {
|
|
if err := validateRequestScopeFields(rs); err == nil {
|
|
t.Errorf("validateRequestScopeFields(%v) = nil, want error", rs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseRawScope(t *testing.T) {
|
|
if _, err := parseRawScope(`{"allow_all":true}`); err != nil {
|
|
t.Errorf("valid object errored: %v", err)
|
|
}
|
|
for _, raw := range []string{`["x"]`, `"s"`, `123`, `{"foo":1}`, `{bad`} {
|
|
if _, err := parseRawScope(raw); err == nil {
|
|
t.Errorf("parseRawScope(%q) = nil, want error", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseScopeAPI_Rejects(t *testing.T) {
|
|
bad := []string{"TRACE /openapi/x", "CONNECT /x", "GET ../admin", "GET openapi/x", "GET /a//b"}
|
|
for _, in := range bad {
|
|
if _, err := parseScopeAPI(in); err == nil {
|
|
t.Errorf("parseScopeAPI(%q) = nil, want error", in)
|
|
}
|
|
}
|
|
// regression: legitimate input still parses (and lowercases the method)
|
|
info, err := parseScopeAPI("get /openapi/orders")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if info["http_method"] != "GET" || info["http_path"] != "/openapi/orders" {
|
|
t.Errorf("info = %v", info)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestScope_RawValidation(t *testing.T) {
|
|
// unknown field now rejected (HIGH-2)
|
|
if _, err := buildRequestScope(false, nil, `{"foo":1}`); err == nil {
|
|
t.Errorf("raw scope with unknown field must error")
|
|
}
|
|
// non-object rejected
|
|
if _, err := buildRequestScope(false, nil, `["x"]`); err == nil {
|
|
t.Errorf("non-object raw scope must error")
|
|
}
|
|
// nested bad method rejected
|
|
if _, err := buildRequestScope(false, nil, `{"http_infos":[{"http_method":"TRACE","http_path":"/x"}]}`); err == nil {
|
|
t.Errorf("raw scope with bad nested method must error")
|
|
}
|
|
// regression: documented fields pass
|
|
if _, err := buildRequestScope(false, nil, `{"allow_all":true}`); err != nil {
|
|
t.Errorf("valid raw scope errored: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequestScope(t *testing.T) {
|
|
t.Run("nothing set -> nil", func(t *testing.T) {
|
|
rs, err := buildRequestScope(false, nil, "")
|
|
if err != nil || rs != nil {
|
|
t.Fatalf("expected nil,nil got rs=%v err=%v", rs, err)
|
|
}
|
|
})
|
|
t.Run("scope-all only", func(t *testing.T) {
|
|
rs, err := buildRequestScope(true, nil, "")
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
m := rs.(map[string]interface{})
|
|
if m["allow_all"] != true {
|
|
t.Errorf("allow_all = %v, want true", m["allow_all"])
|
|
}
|
|
if _, ok := m["http_infos"]; ok {
|
|
t.Errorf("http_infos should not appear when no scope-api provided")
|
|
}
|
|
})
|
|
t.Run("scope-api adds http_infos", func(t *testing.T) {
|
|
rs, err := buildRequestScope(false, []string{"GET /openapi/x"}, "")
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
m := rs.(map[string]interface{})
|
|
if m["allow_all"] != false {
|
|
t.Errorf("allow_all = %v, want false", m["allow_all"])
|
|
}
|
|
infos := m["http_infos"].([]interface{})
|
|
if len(infos) != 1 {
|
|
t.Fatalf("http_infos len = %d, want 1", len(infos))
|
|
}
|
|
info := infos[0].(map[string]interface{})
|
|
if info["http_method"] != "GET" || info["http_path"] != "/openapi/x" {
|
|
t.Errorf("info = %v", info)
|
|
}
|
|
})
|
|
t.Run("raw scope passthrough", func(t *testing.T) {
|
|
rs, err := buildRequestScope(false, nil, `{"allow_all":true}`)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
m := rs.(map[string]interface{})
|
|
if m["allow_all"] != true {
|
|
t.Errorf("allow_all = %v, want true", m["allow_all"])
|
|
}
|
|
})
|
|
t.Run("raw + scope-all -> error", func(t *testing.T) {
|
|
if _, err := buildRequestScope(true, nil, `{"allow_all":true}`); err == nil {
|
|
t.Errorf("raw + scope-all must error")
|
|
}
|
|
})
|
|
t.Run("raw + scope-api -> error", func(t *testing.T) {
|
|
if _, err := buildRequestScope(false, []string{"GET /openapi/x"}, `{"allow_all":true}`); err == nil {
|
|
t.Errorf("raw + scope-api must error")
|
|
}
|
|
})
|
|
t.Run("invalid raw json -> error", func(t *testing.T) {
|
|
if _, err := buildRequestScope(false, nil, "{bad"); err == nil {
|
|
t.Errorf("invalid json must error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBuildKeyConfig(t *testing.T) {
|
|
t.Run("nothing set -> nil", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(false, nil, "", false, false)
|
|
if err != nil || cfg != nil {
|
|
t.Fatalf("empty -> nil, got cfg=%v err=%v", cfg, err)
|
|
}
|
|
})
|
|
t.Run("scope-all -> snake_case request_scope", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(true, nil, "", false, false)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
rs := cfg["request_scope"].(map[string]interface{})
|
|
if rs["allow_all"] != true {
|
|
t.Errorf("allow_all = %v, want true", rs["allow_all"])
|
|
}
|
|
if _, ok := cfg["is_allow_access_preview"]; ok {
|
|
t.Errorf("is_allow_access_preview should not appear")
|
|
}
|
|
})
|
|
t.Run("scope-api -> snake_case http_infos", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(false, []string{"GET /openapi/x"}, "", false, false)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
rs := cfg["request_scope"].(map[string]interface{})
|
|
if rs["allow_all"] != false {
|
|
t.Errorf("allow_all = %v, want false", rs["allow_all"])
|
|
}
|
|
infos := rs["http_infos"].([]interface{})
|
|
if len(infos) != 1 {
|
|
t.Fatalf("http_infos len = %d, want 1", len(infos))
|
|
}
|
|
info := infos[0].(map[string]interface{})
|
|
if info["http_method"] != "GET" || info["http_path"] != "/openapi/x" {
|
|
t.Errorf("info = %v", info)
|
|
}
|
|
})
|
|
t.Run("raw scope passthrough", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(false, nil, `{"allow_all":true}`, false, false)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
rs := cfg["request_scope"].(map[string]interface{})
|
|
if rs["allow_all"] != true {
|
|
t.Errorf("allow_all = %v", rs["allow_all"])
|
|
}
|
|
})
|
|
t.Run("allow-preview only -> is_allow_access_preview", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(false, nil, "", true, true)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if _, ok := cfg["request_scope"]; ok {
|
|
t.Errorf("request_scope should not appear when not set")
|
|
}
|
|
if cfg["is_allow_access_preview"] != true {
|
|
t.Errorf("is_allow_access_preview = %v, want true", cfg["is_allow_access_preview"])
|
|
}
|
|
})
|
|
t.Run("scope-all + allow-preview -> both snake_case keys", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(true, nil, "", true, false)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if _, ok := cfg["request_scope"]; !ok {
|
|
t.Errorf("request_scope missing")
|
|
}
|
|
if cfg["is_allow_access_preview"] != false {
|
|
t.Errorf("is_allow_access_preview = %v, want false", cfg["is_allow_access_preview"])
|
|
}
|
|
// ensure no camelCase keys
|
|
if _, ok := cfg["requestScope"]; ok {
|
|
t.Errorf("found camelCase key requestScope — must use snake_case")
|
|
}
|
|
if _, ok := cfg["isAllowAccessPreview"]; ok {
|
|
t.Errorf("found camelCase key isAllowAccessPreview — must use snake_case")
|
|
}
|
|
})
|
|
t.Run("raw + scope-all -> error", func(t *testing.T) {
|
|
if _, err := buildKeyConfig(true, nil, `{"allow_all":true}`, false, false); err == nil {
|
|
t.Errorf("raw + scope-all must error")
|
|
}
|
|
})
|
|
t.Run("invalid json -> error", func(t *testing.T) {
|
|
if _, err := buildKeyConfig(false, nil, "{bad", false, false); err == nil {
|
|
t.Errorf("invalid json must error")
|
|
}
|
|
})
|
|
t.Run("no camelCase keys emitted", func(t *testing.T) {
|
|
cfg, err := buildKeyConfig(false, []string{"GET /openapi/x"}, "", true, true)
|
|
if err != nil {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
if _, ok := cfg["requestScope"]; ok {
|
|
t.Errorf("camelCase requestScope must not appear")
|
|
}
|
|
if _, ok := cfg["isAllowAccessPreview"]; ok {
|
|
t.Errorf("camelCase isAllowAccessPreview must not appear")
|
|
}
|
|
rs := cfg["request_scope"].(map[string]interface{})
|
|
infos := rs["http_infos"].([]interface{})
|
|
info := infos[0].(map[string]interface{})
|
|
wantInfo := map[string]interface{}{"http_method": "GET", "http_path": "/openapi/x"}
|
|
if !reflect.DeepEqual(info, wantInfo) {
|
|
t.Errorf("info = %v, want %v", info, wantInfo)
|
|
}
|
|
})
|
|
}
|