mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
feat(registry): add SetEmbeddedMeta with parse-order guard
This commit is contained in:
@@ -6,6 +6,8 @@ package registry
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -31,6 +33,43 @@ var (
|
||||
embeddedParseOnce sync.Once
|
||||
)
|
||||
|
||||
// ErrMetaAlreadyLoaded is returned by SetEmbeddedMeta when the embedded
|
||||
// metadata has already been parsed; injection must happen before any
|
||||
// registry consumption. extension/apimeta re-exports it as ErrAlreadyLoaded.
|
||||
var ErrMetaAlreadyLoaded = errors.New("embedded api metadata already parsed")
|
||||
|
||||
var (
|
||||
// embeddedInjectMu serializes SetEmbeddedMeta against the first parse so
|
||||
// check-then-write and mark-then-parse never interleave: an injection
|
||||
// either fully lands before the parse or fails with ErrMetaAlreadyLoaded.
|
||||
embeddedInjectMu sync.Mutex
|
||||
embeddedParsed bool // set inside parseEmbedded's Once body
|
||||
)
|
||||
|
||||
// SetEmbeddedMeta validates data and installs it as this process's embedded
|
||||
// API metadata — the same variable go:embed fills in official builds, so every
|
||||
// downstream consumer (schema, command generation, scope discovery, cache
|
||||
// overlay version gating) behaves exactly as an official build would.
|
||||
//
|
||||
// It is the internal engine of extension/apimeta.SetEmbedded; see that
|
||||
// package for the public contract.
|
||||
func SetEmbeddedMeta(data []byte) error {
|
||||
reg, err := meta.Parse(data) // validate before write; meta.Parse(nil/empty) returns a zero Registry with no error
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid api metadata: %w", err)
|
||||
}
|
||||
if len(reg.Services) == 0 {
|
||||
return errors.New("api metadata contains no services")
|
||||
}
|
||||
embeddedInjectMu.Lock()
|
||||
defer embeddedInjectMu.Unlock()
|
||||
if embeddedParsed {
|
||||
return ErrMetaAlreadyLoaded
|
||||
}
|
||||
embeddedMetaJSON = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseEmbedded decodes the embedded meta_data.json into the typed model exactly
|
||||
// once. It is the single parse of the embedded bytes: both the overlay-free
|
||||
// envelope path (EmbeddedServicesTyped) and the merged command/scope path
|
||||
@@ -38,6 +77,9 @@ var (
|
||||
// twice and no map round-trip is needed downstream.
|
||||
func parseEmbedded() {
|
||||
embeddedParseOnce.Do(func() {
|
||||
embeddedInjectMu.Lock()
|
||||
embeddedParsed = true
|
||||
embeddedInjectMu.Unlock()
|
||||
reg, _ := meta.Parse(embeddedMetaJSON)
|
||||
embeddedVersion = reg.Version
|
||||
embeddedServices = reg.Services
|
||||
|
||||
126
internal/registry/loader_inject_test.go
Normal file
126
internal/registry/loader_inject_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/apicatalog"
|
||||
)
|
||||
|
||||
const injectValidMetaJSON = `{"version":"9.9.9","services":[{"name":"testsvc","title":"Test Service","resources":{}}]}`
|
||||
|
||||
// resetInjectState resets package state for injection tests and restores the
|
||||
// original embedded bytes afterwards (same save/restore pattern as
|
||||
// catalog_test.go). resetInit() itself clears embeddedParsed.
|
||||
func resetInjectState(t *testing.T) {
|
||||
t.Helper()
|
||||
orig := embeddedMetaJSON
|
||||
resetInit()
|
||||
embeddedServices = nil
|
||||
embeddedServicesByName = nil
|
||||
t.Cleanup(func() {
|
||||
resetInit()
|
||||
embeddedServices = nil
|
||||
embeddedServicesByName = nil
|
||||
embeddedMetaJSON = orig
|
||||
})
|
||||
}
|
||||
|
||||
// R1: parse 前注入合法 meta → 生效,version 基线更新
|
||||
func TestSetEmbeddedMeta_InjectBeforeParse(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
if err := SetEmbeddedMeta([]byte(injectValidMetaJSON)); err != nil {
|
||||
t.Fatalf("SetEmbeddedMeta() = %v, want nil", err)
|
||||
}
|
||||
svcs := EmbeddedServicesTyped()
|
||||
if len(svcs) != 1 || svcs[0].Name != "testsvc" {
|
||||
t.Fatalf("EmbeddedServicesTyped() = %+v, want single service testsvc", svcs)
|
||||
}
|
||||
if embeddedVersion != "9.9.9" { // R7: overlay 门禁的比较基线来源正确
|
||||
t.Fatalf("embeddedVersion = %q, want %q", embeddedVersion, "9.9.9")
|
||||
}
|
||||
}
|
||||
|
||||
// R1b: parse 前多次注入 → 后写覆盖(注入者赢,spec §3.2 末行)
|
||||
func TestSetEmbeddedMeta_LastWriteWinsBeforeParse(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
first := `{"version":"1.0.0","services":[{"name":"firstsvc","resources":{}}]}`
|
||||
if err := SetEmbeddedMeta([]byte(first)); err != nil {
|
||||
t.Fatalf("SetEmbeddedMeta(first) = %v, want nil", err)
|
||||
}
|
||||
if err := SetEmbeddedMeta([]byte(injectValidMetaJSON)); err != nil {
|
||||
t.Fatalf("SetEmbeddedMeta(second) = %v, want nil", err)
|
||||
}
|
||||
svcs := EmbeddedServicesTyped()
|
||||
if len(svcs) != 1 || svcs[0].Name != "testsvc" {
|
||||
t.Fatalf("EmbeddedServicesTyped() = %+v, want last-injected testsvc", svcs)
|
||||
}
|
||||
}
|
||||
|
||||
// R2: 注入后 SchemaCatalog 走 embedded 快路径
|
||||
func TestSetEmbeddedMeta_SchemaCatalogUsesEmbedded(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
if err := SetEmbeddedMeta([]byte(injectValidMetaJSON)); err != nil {
|
||||
t.Fatalf("SetEmbeddedMeta() = %v, want nil", err)
|
||||
}
|
||||
cat := SchemaCatalog()
|
||||
if cat.Source() != apicatalog.SourceEmbedded {
|
||||
t.Fatalf("SchemaCatalog().Source() = %q, want %q", cat.Source(), apicatalog.SourceEmbedded)
|
||||
}
|
||||
if _, ok := cat.Service("testsvc"); !ok {
|
||||
t.Fatalf("SchemaCatalog() missing injected service testsvc")
|
||||
}
|
||||
}
|
||||
|
||||
// R3: 非法 JSON 拒绝且状态不变
|
||||
func TestSetEmbeddedMeta_InvalidJSONRejected(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
before := embeddedMetaJSON
|
||||
err := SetEmbeddedMeta([]byte(`{"broken`))
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid api metadata") {
|
||||
t.Fatalf("SetEmbeddedMeta(invalid) = %v, want invalid api metadata error", err)
|
||||
}
|
||||
if string(embeddedMetaJSON) != string(before) {
|
||||
t.Fatalf("embeddedMetaJSON mutated on rejected input")
|
||||
}
|
||||
}
|
||||
|
||||
// R4: 合法 JSON 但 services 为空 → 拒绝且状态不变
|
||||
func TestSetEmbeddedMeta_EmptyServicesRejected(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
before := embeddedMetaJSON
|
||||
for _, in := range []string{`{}`, `{"version":"1.0.0","services":[]}`} {
|
||||
err := SetEmbeddedMeta([]byte(in))
|
||||
if err == nil || !strings.Contains(err.Error(), "api metadata contains no services") {
|
||||
t.Fatalf("SetEmbeddedMeta(%q) = %v, want no-services error", in, err)
|
||||
}
|
||||
}
|
||||
if string(embeddedMetaJSON) != string(before) {
|
||||
t.Fatalf("embeddedMetaJSON mutated on rejected input")
|
||||
}
|
||||
}
|
||||
|
||||
// R5: 首次 parse 之后注入 → ErrMetaAlreadyLoaded
|
||||
func TestSetEmbeddedMeta_AfterParseRejected(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
_ = EmbeddedServicesTyped() // 触发首次 parse
|
||||
err := SetEmbeddedMeta([]byte(injectValidMetaJSON))
|
||||
if !errors.Is(err, ErrMetaAlreadyLoaded) {
|
||||
t.Fatalf("SetEmbeddedMeta(after parse) = %v, want ErrMetaAlreadyLoaded", err)
|
||||
}
|
||||
}
|
||||
|
||||
// R6: nil / 空字节 → 恒走 services 为空路径(meta.Parse 空输入返回零值无错误)
|
||||
func TestSetEmbeddedMeta_NilAndEmptyRejected(t *testing.T) {
|
||||
resetInjectState(t)
|
||||
for _, in := range [][]byte{nil, {}} {
|
||||
err := SetEmbeddedMeta(in)
|
||||
if err == nil || !strings.Contains(err.Error(), "api metadata contains no services") {
|
||||
t.Fatalf("SetEmbeddedMeta(len=%d) = %v, want no-services error", len(in), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ func resetInit() {
|
||||
waitBackgroundRefresh()
|
||||
initOnce = sync.Once{}
|
||||
embeddedParseOnce = sync.Once{}
|
||||
embeddedInjectMu.Lock()
|
||||
embeddedParsed = false
|
||||
embeddedInjectMu.Unlock()
|
||||
servicesTypedOnce = sync.Once{}
|
||||
servicesTyped = nil
|
||||
mergedServices = make(map[string]meta.Service)
|
||||
|
||||
Reference in New Issue
Block a user