mirror of
https://codeberg.org/Codeberg/pages-server.git
synced 2024-11-06 06:17:02 +00:00
77a8439ea7
This PR renames `gitea` in cli args to `forge` and `GITEA` in environment variables to `FORGE` and adds the gitea names as aliases for the forge names. Also closes #311 Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/339
630 lines
17 KiB
Go
630 lines
17 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/urfave/cli/v2"
|
|
|
|
cmd "codeberg.org/codeberg/pages/cli"
|
|
)
|
|
|
|
func runApp(t *testing.T, fn func(*cli.Context) error, args []string) {
|
|
app := cmd.CreatePagesApp()
|
|
app.Action = fn
|
|
|
|
appCtx, appCancel := context.WithCancel(context.Background())
|
|
defer appCancel()
|
|
|
|
// os.Args always contains the binary name
|
|
args = append([]string{"testing"}, args...)
|
|
|
|
err := app.RunContext(appCtx, args)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// fixArrayFromCtx fixes the number of "changed" strings in a string slice according to the number of values in the context.
|
|
// This is a workaround because the cli library has a bug where the number of values in the context gets bigger the more tests are run.
|
|
func fixArrayFromCtx(ctx *cli.Context, key string, expected []string) []string {
|
|
if ctx.IsSet(key) {
|
|
ctxSlice := ctx.StringSlice(key)
|
|
|
|
if len(ctxSlice) > 1 {
|
|
for i := 1; i < len(ctxSlice); i++ {
|
|
expected = append([]string{"changed"}, expected...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return expected
|
|
}
|
|
|
|
func readTestConfig() (*Config, error) {
|
|
content, err := os.ReadFile("assets/test_config.toml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
expectedConfig := NewDefaultConfig()
|
|
err = toml.Unmarshal(content, &expectedConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &expectedConfig, nil
|
|
}
|
|
|
|
func TestReadConfigShouldReturnEmptyConfigWhenConfigArgEmpty(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg, err := ReadConfig(ctx)
|
|
expected := NewDefaultConfig()
|
|
assert.Equal(t, &expected, cfg)
|
|
|
|
return err
|
|
},
|
|
[]string{},
|
|
)
|
|
}
|
|
|
|
func TestReadConfigShouldReturnConfigFromFileWhenConfigArgPresent(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg, err := ReadConfig(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expectedConfig, err := readTestConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{"--config-file", "assets/test_config.toml"},
|
|
)
|
|
}
|
|
|
|
func TestValuesReadFromConfigFileShouldBeOverwrittenByArgs(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg, err := ReadConfig(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
MergeConfig(ctx, cfg)
|
|
|
|
expectedConfig, err := readTestConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expectedConfig.LogLevel = "debug"
|
|
expectedConfig.Forge.Root = "not-codeberg.org"
|
|
expectedConfig.ACME.AcceptTerms = true
|
|
expectedConfig.Server.Host = "172.17.0.2"
|
|
expectedConfig.Server.BlacklistedPaths = append(expectedConfig.Server.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--config-file", "assets/test_config.toml",
|
|
"--log-level", "debug",
|
|
"--forge-root", "not-codeberg.org",
|
|
"--acme-accept-terms",
|
|
"--host", "172.17.0.2",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &Config{
|
|
LogLevel: "original",
|
|
Server: ServerConfig{
|
|
Host: "original",
|
|
Port: 8080,
|
|
HttpPort: 80,
|
|
HttpServerEnabled: false,
|
|
MainDomain: "original",
|
|
RawDomain: "original",
|
|
PagesBranches: []string{"original"},
|
|
AllowedCorsDomains: []string{"original"},
|
|
BlacklistedPaths: []string{"original"},
|
|
},
|
|
Forge: ForgeConfig{
|
|
Root: "original",
|
|
Token: "original",
|
|
LFSEnabled: false,
|
|
FollowSymlinks: false,
|
|
DefaultMimeType: "original",
|
|
ForbiddenMimeTypes: []string{"original"},
|
|
},
|
|
Database: DatabaseConfig{
|
|
Type: "original",
|
|
Conn: "original",
|
|
},
|
|
ACME: ACMEConfig{
|
|
Email: "original",
|
|
APIEndpoint: "original",
|
|
AcceptTerms: false,
|
|
UseRateLimits: false,
|
|
EAB_HMAC: "original",
|
|
EAB_KID: "original",
|
|
DNSProvider: "original",
|
|
NoDNS01: false,
|
|
AccountConfigFile: "original",
|
|
},
|
|
}
|
|
|
|
MergeConfig(ctx, cfg)
|
|
|
|
expectedConfig := &Config{
|
|
LogLevel: "changed",
|
|
Server: ServerConfig{
|
|
Host: "changed",
|
|
Port: 8443,
|
|
HttpPort: 443,
|
|
HttpServerEnabled: true,
|
|
MainDomain: "changed",
|
|
RawDomain: "changed",
|
|
PagesBranches: []string{"changed"},
|
|
AllowedCorsDomains: []string{"changed"},
|
|
BlacklistedPaths: append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...),
|
|
},
|
|
Forge: ForgeConfig{
|
|
Root: "changed",
|
|
Token: "changed",
|
|
LFSEnabled: true,
|
|
FollowSymlinks: true,
|
|
DefaultMimeType: "changed",
|
|
ForbiddenMimeTypes: []string{"changed"},
|
|
},
|
|
Database: DatabaseConfig{
|
|
Type: "changed",
|
|
Conn: "changed",
|
|
},
|
|
ACME: ACMEConfig{
|
|
Email: "changed",
|
|
APIEndpoint: "changed",
|
|
AcceptTerms: true,
|
|
UseRateLimits: true,
|
|
EAB_HMAC: "changed",
|
|
EAB_KID: "changed",
|
|
DNSProvider: "changed",
|
|
NoDNS01: true,
|
|
AccountConfigFile: "changed",
|
|
},
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--log-level", "changed",
|
|
// Server
|
|
"--pages-domain", "changed",
|
|
"--raw-domain", "changed",
|
|
"--allowed-cors-domains", "changed",
|
|
"--blacklisted-paths", "changed",
|
|
"--pages-branch", "changed",
|
|
"--host", "changed",
|
|
"--port", "8443",
|
|
"--http-port", "443",
|
|
"--enable-http-server",
|
|
// Forge
|
|
"--forge-root", "changed",
|
|
"--forge-api-token", "changed",
|
|
"--enable-lfs-support",
|
|
"--enable-symlink-support",
|
|
"--default-mime-type", "changed",
|
|
"--forbidden-mime-types", "changed",
|
|
// Database
|
|
"--db-type", "changed",
|
|
"--db-conn", "changed",
|
|
// ACME
|
|
"--acme-email", "changed",
|
|
"--acme-api-endpoint", "changed",
|
|
"--acme-accept-terms",
|
|
"--acme-use-rate-limits",
|
|
"--acme-eab-hmac", "changed",
|
|
"--acme-eab-kid", "changed",
|
|
"--dns-provider", "changed",
|
|
"--no-dns-01",
|
|
"--acme-account-config", "changed",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeServerConfigShouldAddDefaultBlacklistedPathsToBlacklistedPaths(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &ServerConfig{}
|
|
mergeServerConfig(ctx, cfg)
|
|
|
|
expected := ALWAYS_BLACKLISTED_PATHS
|
|
assert.Equal(t, expected, cfg.BlacklistedPaths)
|
|
|
|
return nil
|
|
},
|
|
[]string{},
|
|
)
|
|
}
|
|
|
|
func TestMergeServerConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
|
for range []uint8{0, 1} {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &ServerConfig{
|
|
Host: "original",
|
|
Port: 8080,
|
|
HttpPort: 80,
|
|
HttpServerEnabled: false,
|
|
MainDomain: "original",
|
|
RawDomain: "original",
|
|
AllowedCorsDomains: []string{"original"},
|
|
BlacklistedPaths: []string{"original"},
|
|
}
|
|
|
|
mergeServerConfig(ctx, cfg)
|
|
|
|
expectedConfig := &ServerConfig{
|
|
Host: "changed",
|
|
Port: 8443,
|
|
HttpPort: 443,
|
|
HttpServerEnabled: true,
|
|
MainDomain: "changed",
|
|
RawDomain: "changed",
|
|
AllowedCorsDomains: fixArrayFromCtx(ctx, "allowed-cors-domains", []string{"changed"}),
|
|
BlacklistedPaths: fixArrayFromCtx(ctx, "blacklisted-paths", append([]string{"changed"}, ALWAYS_BLACKLISTED_PATHS...)),
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--pages-domain", "changed",
|
|
"--raw-domain", "changed",
|
|
"--allowed-cors-domains", "changed",
|
|
"--blacklisted-paths", "changed",
|
|
"--host", "changed",
|
|
"--port", "8443",
|
|
"--http-port", "443",
|
|
"--enable-http-server",
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMergeServerConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
|
type testValuePair struct {
|
|
args []string
|
|
callback func(*ServerConfig)
|
|
}
|
|
testValuePairs := []testValuePair{
|
|
{args: []string{"--host", "changed"}, callback: func(sc *ServerConfig) { sc.Host = "changed" }},
|
|
{args: []string{"--port", "8443"}, callback: func(sc *ServerConfig) { sc.Port = 8443 }},
|
|
{args: []string{"--http-port", "443"}, callback: func(sc *ServerConfig) { sc.HttpPort = 443 }},
|
|
{args: []string{"--enable-http-server"}, callback: func(sc *ServerConfig) { sc.HttpServerEnabled = true }},
|
|
{args: []string{"--pages-domain", "changed"}, callback: func(sc *ServerConfig) { sc.MainDomain = "changed" }},
|
|
{args: []string{"--raw-domain", "changed"}, callback: func(sc *ServerConfig) { sc.RawDomain = "changed" }},
|
|
{args: []string{"--pages-branch", "changed"}, callback: func(sc *ServerConfig) { sc.PagesBranches = []string{"changed"} }},
|
|
{args: []string{"--allowed-cors-domains", "changed"}, callback: func(sc *ServerConfig) { sc.AllowedCorsDomains = []string{"changed"} }},
|
|
{args: []string{"--blacklisted-paths", "changed"}, callback: func(sc *ServerConfig) { sc.BlacklistedPaths = []string{"changed"} }},
|
|
}
|
|
|
|
for _, pair := range testValuePairs {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := ServerConfig{
|
|
Host: "original",
|
|
Port: 8080,
|
|
HttpPort: 80,
|
|
HttpServerEnabled: false,
|
|
MainDomain: "original",
|
|
RawDomain: "original",
|
|
PagesBranches: []string{"original"},
|
|
AllowedCorsDomains: []string{"original"},
|
|
BlacklistedPaths: []string{"original"},
|
|
}
|
|
|
|
expectedConfig := cfg
|
|
pair.callback(&expectedConfig)
|
|
expectedConfig.BlacklistedPaths = append(expectedConfig.BlacklistedPaths, ALWAYS_BLACKLISTED_PATHS...)
|
|
|
|
expectedConfig.PagesBranches = fixArrayFromCtx(ctx, "pages-branch", expectedConfig.PagesBranches)
|
|
expectedConfig.AllowedCorsDomains = fixArrayFromCtx(ctx, "allowed-cors-domains", expectedConfig.AllowedCorsDomains)
|
|
expectedConfig.BlacklistedPaths = fixArrayFromCtx(ctx, "blacklisted-paths", expectedConfig.BlacklistedPaths)
|
|
|
|
mergeServerConfig(ctx, &cfg)
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
pair.args,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMergeForgeConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &ForgeConfig{
|
|
Root: "original",
|
|
Token: "original",
|
|
LFSEnabled: false,
|
|
FollowSymlinks: false,
|
|
DefaultMimeType: "original",
|
|
ForbiddenMimeTypes: []string{"original"},
|
|
}
|
|
|
|
mergeForgeConfig(ctx, cfg)
|
|
|
|
expectedConfig := &ForgeConfig{
|
|
Root: "changed",
|
|
Token: "changed",
|
|
LFSEnabled: true,
|
|
FollowSymlinks: true,
|
|
DefaultMimeType: "changed",
|
|
ForbiddenMimeTypes: fixArrayFromCtx(ctx, "forbidden-mime-types", []string{"changed"}),
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--forge-root", "changed",
|
|
"--forge-api-token", "changed",
|
|
"--enable-lfs-support",
|
|
"--enable-symlink-support",
|
|
"--default-mime-type", "changed",
|
|
"--forbidden-mime-types", "changed",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeForgeConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
|
type testValuePair struct {
|
|
args []string
|
|
callback func(*ForgeConfig)
|
|
}
|
|
testValuePairs := []testValuePair{
|
|
{args: []string{"--forge-root", "changed"}, callback: func(gc *ForgeConfig) { gc.Root = "changed" }},
|
|
{args: []string{"--forge-api-token", "changed"}, callback: func(gc *ForgeConfig) { gc.Token = "changed" }},
|
|
{args: []string{"--enable-lfs-support"}, callback: func(gc *ForgeConfig) { gc.LFSEnabled = true }},
|
|
{args: []string{"--enable-symlink-support"}, callback: func(gc *ForgeConfig) { gc.FollowSymlinks = true }},
|
|
{args: []string{"--default-mime-type", "changed"}, callback: func(gc *ForgeConfig) { gc.DefaultMimeType = "changed" }},
|
|
{args: []string{"--forbidden-mime-types", "changed"}, callback: func(gc *ForgeConfig) { gc.ForbiddenMimeTypes = []string{"changed"} }},
|
|
}
|
|
|
|
for _, pair := range testValuePairs {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := ForgeConfig{
|
|
Root: "original",
|
|
Token: "original",
|
|
LFSEnabled: false,
|
|
FollowSymlinks: false,
|
|
DefaultMimeType: "original",
|
|
ForbiddenMimeTypes: []string{"original"},
|
|
}
|
|
|
|
expectedConfig := cfg
|
|
pair.callback(&expectedConfig)
|
|
|
|
mergeForgeConfig(ctx, &cfg)
|
|
|
|
expectedConfig.ForbiddenMimeTypes = fixArrayFromCtx(ctx, "forbidden-mime-types", expectedConfig.ForbiddenMimeTypes)
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
pair.args,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMergeForgeConfigShouldReplaceValuesGivenGiteaOptionsExist(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &ForgeConfig{
|
|
Root: "original",
|
|
Token: "original",
|
|
}
|
|
|
|
mergeForgeConfig(ctx, cfg)
|
|
|
|
expectedConfig := &ForgeConfig{
|
|
Root: "changed",
|
|
Token: "changed",
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--gitea-root", "changed",
|
|
"--gitea-api-token", "changed",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeDatabaseConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &DatabaseConfig{
|
|
Type: "original",
|
|
Conn: "original",
|
|
}
|
|
|
|
mergeDatabaseConfig(ctx, cfg)
|
|
|
|
expectedConfig := &DatabaseConfig{
|
|
Type: "changed",
|
|
Conn: "changed",
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--db-type", "changed",
|
|
"--db-conn", "changed",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeDatabaseConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
|
type testValuePair struct {
|
|
args []string
|
|
callback func(*DatabaseConfig)
|
|
}
|
|
testValuePairs := []testValuePair{
|
|
{args: []string{"--db-type", "changed"}, callback: func(gc *DatabaseConfig) { gc.Type = "changed" }},
|
|
{args: []string{"--db-conn", "changed"}, callback: func(gc *DatabaseConfig) { gc.Conn = "changed" }},
|
|
}
|
|
|
|
for _, pair := range testValuePairs {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := DatabaseConfig{
|
|
Type: "original",
|
|
Conn: "original",
|
|
}
|
|
|
|
expectedConfig := cfg
|
|
pair.callback(&expectedConfig)
|
|
|
|
mergeDatabaseConfig(ctx, &cfg)
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
pair.args,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestMergeACMEConfigShouldReplaceAllExistingValuesGivenAllArgsExist(t *testing.T) {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := &ACMEConfig{
|
|
Email: "original",
|
|
APIEndpoint: "original",
|
|
AcceptTerms: false,
|
|
UseRateLimits: false,
|
|
EAB_HMAC: "original",
|
|
EAB_KID: "original",
|
|
DNSProvider: "original",
|
|
NoDNS01: false,
|
|
AccountConfigFile: "original",
|
|
}
|
|
|
|
mergeACMEConfig(ctx, cfg)
|
|
|
|
expectedConfig := &ACMEConfig{
|
|
Email: "changed",
|
|
APIEndpoint: "changed",
|
|
AcceptTerms: true,
|
|
UseRateLimits: true,
|
|
EAB_HMAC: "changed",
|
|
EAB_KID: "changed",
|
|
DNSProvider: "changed",
|
|
NoDNS01: true,
|
|
AccountConfigFile: "changed",
|
|
}
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
[]string{
|
|
"--acme-email", "changed",
|
|
"--acme-api-endpoint", "changed",
|
|
"--acme-accept-terms",
|
|
"--acme-use-rate-limits",
|
|
"--acme-eab-hmac", "changed",
|
|
"--acme-eab-kid", "changed",
|
|
"--dns-provider", "changed",
|
|
"--no-dns-01",
|
|
"--acme-account-config", "changed",
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestMergeACMEConfigShouldReplaceOnlyOneValueExistingValueGivenOnlyOneArgExists(t *testing.T) {
|
|
type testValuePair struct {
|
|
args []string
|
|
callback func(*ACMEConfig)
|
|
}
|
|
testValuePairs := []testValuePair{
|
|
{args: []string{"--acme-email", "changed"}, callback: func(gc *ACMEConfig) { gc.Email = "changed" }},
|
|
{args: []string{"--acme-api-endpoint", "changed"}, callback: func(gc *ACMEConfig) { gc.APIEndpoint = "changed" }},
|
|
{args: []string{"--acme-accept-terms"}, callback: func(gc *ACMEConfig) { gc.AcceptTerms = true }},
|
|
{args: []string{"--acme-use-rate-limits"}, callback: func(gc *ACMEConfig) { gc.UseRateLimits = true }},
|
|
{args: []string{"--acme-eab-hmac", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_HMAC = "changed" }},
|
|
{args: []string{"--acme-eab-kid", "changed"}, callback: func(gc *ACMEConfig) { gc.EAB_KID = "changed" }},
|
|
{args: []string{"--dns-provider", "changed"}, callback: func(gc *ACMEConfig) { gc.DNSProvider = "changed" }},
|
|
{args: []string{"--no-dns-01"}, callback: func(gc *ACMEConfig) { gc.NoDNS01 = true }},
|
|
{args: []string{"--acme-account-config", "changed"}, callback: func(gc *ACMEConfig) { gc.AccountConfigFile = "changed" }},
|
|
}
|
|
|
|
for _, pair := range testValuePairs {
|
|
runApp(
|
|
t,
|
|
func(ctx *cli.Context) error {
|
|
cfg := ACMEConfig{
|
|
Email: "original",
|
|
APIEndpoint: "original",
|
|
AcceptTerms: false,
|
|
UseRateLimits: false,
|
|
EAB_HMAC: "original",
|
|
EAB_KID: "original",
|
|
DNSProvider: "original",
|
|
AccountConfigFile: "original",
|
|
}
|
|
|
|
expectedConfig := cfg
|
|
pair.callback(&expectedConfig)
|
|
|
|
mergeACMEConfig(ctx, &cfg)
|
|
|
|
assert.Equal(t, expectedConfig, cfg)
|
|
|
|
return nil
|
|
},
|
|
pair.args,
|
|
)
|
|
}
|
|
}
|