Many changes
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -22,32 +25,11 @@ func newGenerateConfigCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func writeDefaultConfig(cmd *cobra.Command) error {
|
||||
cfg := config.Config{
|
||||
Server: config.ServerConfig{
|
||||
ListenAddr: ":8080",
|
||||
},
|
||||
Limits: config.LimitsConfig{
|
||||
MaxUploadSize: "100MB",
|
||||
DefaultTTL: "1h",
|
||||
},
|
||||
Storage: config.StorageConfig{
|
||||
DataDir: "./data",
|
||||
CleanupInterval: "1m",
|
||||
},
|
||||
Security: config.SecurityConfig{
|
||||
AllowedIPs: []string{},
|
||||
TrustProxyHeaders: false,
|
||||
RateLimit: config.RateLimitConfig{
|
||||
Enabled: true,
|
||||
RequestsPerMinute: 30,
|
||||
Burst: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
cfg := config.Default()
|
||||
|
||||
b, err := yaml.Marshal(cfg)
|
||||
b, err := marshalYAMLWithComments(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal default config: %w", err)
|
||||
return fmt.Errorf("marshal default config with comments: %w", err)
|
||||
}
|
||||
|
||||
if _, err := cmd.OutOrStdout().Write(b); err != nil {
|
||||
@@ -56,3 +38,111 @@ func writeDefaultConfig(cmd *cobra.Command) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalYAMLWithComments(v any) ([]byte, error) {
|
||||
root, err := nodeFromValue(reflect.ValueOf(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(4)
|
||||
if err := enc.Encode(root); err != nil {
|
||||
return nil, fmt.Errorf("encode yaml: %w", err)
|
||||
}
|
||||
if err := enc.Close(); err != nil {
|
||||
return nil, fmt.Errorf("close yaml encoder: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func nodeFromValue(value reflect.Value) (*yaml.Node, error) {
|
||||
if !value.IsValid() {
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}, nil
|
||||
}
|
||||
|
||||
for value.Kind() == reflect.Ptr {
|
||||
if value.IsNil() {
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}, nil
|
||||
}
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Struct {
|
||||
return nodeFromStruct(value)
|
||||
}
|
||||
|
||||
return scalarOrCollectionNode(value.Interface())
|
||||
}
|
||||
|
||||
func nodeFromStruct(value reflect.Value) (*yaml.Node, error) {
|
||||
typ := value.Type()
|
||||
mapping := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
key, skip := yamlFieldName(field)
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
|
||||
keyNode := &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: "!!str",
|
||||
Value: key,
|
||||
}
|
||||
if comment := strings.TrimSpace(field.Tag.Get("comment")); comment != "" {
|
||||
keyNode.HeadComment = comment
|
||||
}
|
||||
|
||||
valueNode, err := nodeFromValue(value.Field(i))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("field %s: %w", field.Name, err)
|
||||
}
|
||||
|
||||
mapping.Content = append(mapping.Content, keyNode, valueNode)
|
||||
}
|
||||
|
||||
return mapping, nil
|
||||
}
|
||||
|
||||
func scalarOrCollectionNode(value any) (*yaml.Node, error) {
|
||||
b, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal value node: %w", err)
|
||||
}
|
||||
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal(b, &doc); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal value node: %w", err)
|
||||
}
|
||||
if len(doc.Content) == 0 {
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}, nil
|
||||
}
|
||||
return doc.Content[0], nil
|
||||
}
|
||||
|
||||
func yamlFieldName(field reflect.StructField) (string, bool) {
|
||||
tag := strings.TrimSpace(field.Tag.Get("yaml"))
|
||||
if tag == "-" {
|
||||
return "", true
|
||||
}
|
||||
if tag == "" {
|
||||
return strings.ToLower(field.Name), false
|
||||
}
|
||||
|
||||
parts := strings.Split(tag, ",")
|
||||
key := strings.TrimSpace(parts[0])
|
||||
if key == "-" {
|
||||
return "", true
|
||||
}
|
||||
if key == "" {
|
||||
return strings.ToLower(field.Name), false
|
||||
}
|
||||
return key, false
|
||||
}
|
||||
|
||||
+14
-53
@@ -16,10 +16,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"scratchbox/internal/config"
|
||||
httpapi "scratchbox/internal/http"
|
||||
"scratchbox/internal/storage"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
@@ -125,44 +125,6 @@ security:
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainProcessHTTPInitFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.yaml")
|
||||
cfg := `
|
||||
server:
|
||||
listen_addr: "127.0.0.1:0"
|
||||
limits:
|
||||
max_upload_size: "1MB"
|
||||
default_ttl: "1h"
|
||||
storage:
|
||||
data_dir: "` + filepath.Join(tmp, "data") + `"
|
||||
cleanup_interval: "10s"
|
||||
security:
|
||||
allowed_ips: []
|
||||
trust_proxy_headers: false
|
||||
rate_limit:
|
||||
enabled: true
|
||||
requests_per_minute: 10
|
||||
burst: 1
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.run=TestMainHelperProcess")
|
||||
cmd.Dir = repoRoot(t)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GO_WANT_MAIN_HELPER=1",
|
||||
"MAIN_HELPER_MODE=http-fail",
|
||||
"MAIN_HELPER_CONFIG="+cfgPath,
|
||||
)
|
||||
if err := cmd.Run(); err == nil {
|
||||
t.Fatalf("expected helper process failure for http init")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMainHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_MAIN_HELPER") != "1" {
|
||||
return
|
||||
@@ -193,14 +155,6 @@ func TestMainHelperProcess(t *testing.T) {
|
||||
os.Args = []string{"scratchbox", "server", "--config", cfgPath}
|
||||
main()
|
||||
os.Exit(0)
|
||||
case "http-fail":
|
||||
cfgPath := os.Getenv("MAIN_HELPER_CONFIG")
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
os.Exit(2)
|
||||
}
|
||||
os.Args = []string{"scratchbox", "server", "--config", cfgPath}
|
||||
main()
|
||||
os.Exit(0)
|
||||
default:
|
||||
os.Exit(2)
|
||||
}
|
||||
@@ -222,17 +176,24 @@ func TestGenerateConfigCommandWritesDefaultYAML(t *testing.T) {
|
||||
if out.Len() == 0 {
|
||||
t.Fatalf("generate-config output is empty")
|
||||
}
|
||||
output := out.String()
|
||||
if !strings.Contains(output, "# HTTP listener settings.") {
|
||||
t.Fatalf("generate-config output missing expected section comment")
|
||||
}
|
||||
if !strings.Contains(output, "# Maximum upload request body size. Supports B, KB, MB, GB and KiB, MiB, GiB (case-insensitive). Defaults to bytes when unit is omitted (example: 1048576).") {
|
||||
t.Fatalf("generate-config output missing expected field comment")
|
||||
}
|
||||
|
||||
var cfg config.Config
|
||||
if err := yaml.Unmarshal(out.Bytes(), &cfg); err != nil {
|
||||
if err := yaml.Unmarshal([]byte(output), &cfg); err != nil {
|
||||
t.Fatalf("yaml.Unmarshal() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server.ListenAddr != ":8080" {
|
||||
t.Fatalf("server.listen_addr = %q, want %q", cfg.Server.ListenAddr, ":8080")
|
||||
}
|
||||
if cfg.Limits.MaxUploadSize != "100MB" {
|
||||
t.Fatalf("limits.max_upload_size = %q, want %q", cfg.Limits.MaxUploadSize, "100MB")
|
||||
if cfg.Limits.MaxUploadSize != "100MiB" {
|
||||
t.Fatalf("limits.max_upload_size = %q, want %q", cfg.Limits.MaxUploadSize, "100MiB")
|
||||
}
|
||||
if cfg.Storage.DataDir != "./data" {
|
||||
t.Fatalf("storage.data_dir = %q, want %q", cfg.Storage.DataDir, "./data")
|
||||
@@ -349,7 +310,7 @@ security:
|
||||
t.Fatalf("create payload missing id")
|
||||
}
|
||||
|
||||
rawResp, err := client.Get(baseURL + "/raw/" + id)
|
||||
rawResp, err := client.Get(baseURL + "/api/raw/" + id)
|
||||
if err != nil {
|
||||
t.Fatalf("client.Get(raw) error = %v", err)
|
||||
}
|
||||
@@ -377,7 +338,7 @@ security:
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll(view) error = %v", err)
|
||||
}
|
||||
if !strings.Contains(string(viewBody), "Scratch "+id) {
|
||||
t.Fatalf("view body missing scratch id")
|
||||
if !strings.Contains(string(viewBody), `id="app"`) {
|
||||
t.Fatalf("view body missing SPA mount point")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user