// Copyright 2018 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package server import ( "errors" "fmt" "strings" "testing" ) func TestConfigCheck(t *testing.T) { tests := []struct { // name is the name of the test. name string // config is content of the configuration file. config string // warningErr is an error that does not prevent server from starting. warningErr error // errorLine is the location of the error. errorLine int // errorPos is the position of the error. errorPos int // warning errors also include a reason optionally. reason string // newDefaultErr is a configuration error that includes source of error. err error }{ { name: "when unknown field is used at top level", config: ` monitor = "127.0.0.1:4442" `, err: errors.New(`unknown field "monitor"`), errorLine: 2, errorPos: 17, }, { name: "when default permissions are used at top level", config: ` "default_permissions" { publish = ["_SANDBOX.>"] subscribe = ["_SANDBOX.>"] } `, err: errors.New(`unknown field "default_permissions"`), errorLine: 2, errorPos: 18, }, { name: "when authorization config is empty", config: ` authorization = { } `, err: nil, }, { name: "when authorization config has unknown fields", config: ` authorization = { foo = "bar" } `, err: errors.New(`unknown field "foo"`), errorLine: 3, errorPos: 5, }, { name: "when authorization config has unknown fields", config: ` port = 4222 authorization = { user = "hello" foo = "bar" password = "world" } `, err: errors.New(`unknown field "foo"`), errorLine: 6, errorPos: 5, }, { name: "when user authorization config has unknown fields", config: ` authorization = { users = [ { user = "foo" pass = "bar" token = "quux" } ] } `, err: errors.New(`unknown field "token"`), errorLine: 7, errorPos: 9, }, { name: "when user authorization permissions config has unknown fields", config: ` authorization { permissions { subscribe = {} inboxes = {} publish = {} } } `, err: errors.New(`Unknown field "inboxes" parsing permissions`), errorLine: 5, errorPos: 7, }, { name: "when user authorization permissions config has unknown fields within allow or deny", config: ` authorization { permissions { subscribe = { allow = ["hello", "world"] deny = ["foo", "bar"] denied = "_INBOX.>" } publish = {} } } `, err: errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`), errorLine: 7, errorPos: 9, }, { name: "when user authorization permissions config has unknown fields within allow or deny", config: ` authorization { permissions { publish = { allow = ["hello", "world"] deny = ["foo", "bar"] allowed = "_INBOX.>" } subscribe = {} } } `, err: errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`), errorLine: 7, errorPos: 9, }, { name: "when user authorization permissions config has unknown fields using arrays", config: ` authorization { default_permissions { subscribe = ["a"] publish = ["b"] inboxes = ["c"] } users = [ { user = "foo" pass = "bar" } ] } `, err: errors.New(`Unknown field "inboxes" parsing permissions`), errorLine: 7, errorPos: 6, }, { name: "when user authorization permissions config has unknown fields using strings", config: ` authorization { default_permissions { subscribe = "a" requests = "b" publish = "c" } users = [ { user = "foo" pass = "bar" } ] } `, err: errors.New(`Unknown field "requests" parsing permissions`), errorLine: 6, errorPos: 6, }, { name: "when user authorization permissions config is empty", config: ` authorization = { users = [ { user = "foo", pass = "bar", permissions = { } } ] } `, err: nil, }, { name: "when unknown permissions are included in user config", config: ` authorization = { users = [ { user = "foo", pass = "bar", permissions { inboxes = true } } ] } `, err: errors.New(`Unknown field "inboxes" parsing permissions`), errorLine: 6, errorPos: 11, }, { name: "when clustering config is empty", config: ` cluster = { } `, err: nil, }, { name: "when unknown option is in clustering config", config: ` # NATS Server Configuration port = 4222 cluster = { port = 6222 foo = "bar" authorization { user = "hello" pass = "world" } } `, err: errors.New(`unknown field "foo"`), errorLine: 9, errorPos: 5, }, { name: "when unknown option is in clustering authorization config", config: ` cluster = { authorization { foo = "bar" } } `, err: errors.New(`unknown field "foo"`), errorLine: 4, errorPos: 7, }, { name: "when unknown option is in tls config", config: ` tls = { hello = "world" } `, err: errors.New(`error parsing tls config, unknown field ["hello"]`), errorLine: 3, errorPos: 5, }, { name: "when unknown option is in cluster tls config", config: ` cluster { tls = { foo = "bar" } } `, err: errors.New(`error parsing tls config, unknown field ["foo"]`), errorLine: 4, errorPos: 7, }, { name: "when using cipher suites in the TLS config", config: ` tls = { cipher_suites: [ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ] preferences = [] } `, err: errors.New(`error parsing tls config, unknown field ["preferences"]`), errorLine: 7, errorPos: 7, }, { name: "when using curve preferences in the TLS config", config: ` tls = { curve_preferences: [ "CurveP256", "CurveP384", "CurveP521" ] suites = [] } `, err: errors.New(`error parsing tls config, unknown field ["suites"]`), errorLine: 8, errorPos: 7, }, { name: "when using curve preferences in the TLS config", config: ` tls = { curve_preferences: [ "CurveP5210000" ] } `, err: errors.New(`unrecognized curve preference CurveP5210000`), errorLine: 4, errorPos: 5, }, { name: "verify_cert_and_check_known_urls not support for clients", config: ` tls = { cert_file: "configs/certs/server.pem" key_file: "configs/certs/key.pem" verify_cert_and_check_known_urls: true } `, err: errors.New("verify_cert_and_check_known_urls not supported in this context"), errorLine: 5, errorPos: 10, }, { name: "when unknown option is in cluster config with defined routes", config: ` cluster { port = 6222 routes = [ nats://127.0.0.1:6222 ] peers = [] } `, err: errors.New(`unknown field "peers"`), errorLine: 7, errorPos: 5, }, { name: "when used as variable in authorization block it should not be considered as unknown field", config: ` # listen: 127.0.0.1:-1 listen: 127.0.0.1:4222 authorization { # Superuser can do anything. super_user = { publish = ">" subscribe = ">" } # Can do requests on foo or bar, and subscribe to anything # that is a response to an _INBOX. # # Notice that authorization filters can be singletons or arrays. req_pub_user = { publish = ["req.foo", "req.bar"] subscribe = "_INBOX.>" } # Setup a default user that can subscribe to anything, but has # no publish capabilities. default_user = { subscribe = "PUBLIC.>" } unused = "hello" # Default permissions if none presented. e.g. susan below. default_permissions: $default_user # Users listed with persmissions. users = [ {user: alice, password: foo, permissions: $super_user} {user: bob, password: bar, permissions: $req_pub_user} {user: susan, password: baz} ] } `, err: errors.New(`unknown field "unused"`), errorLine: 27, errorPos: 5, }, { name: "when used as variable in top level config it should not be considered as unknown field", config: ` monitoring_port = 8222 http_port = $monitoring_port port = 4222 `, err: nil, }, { name: "when used as variable in cluster config it should not be considered as unknown field", config: ` cluster { clustering_port = 6222 port = $clustering_port } `, err: nil, }, { name: "when setting permissions within cluster authorization block", config: ` cluster { authorization { permissions = { publish = { allow = ["foo", "bar"] } } } permissions = { publish = { deny = ["foo", "bar"] } } } `, warningErr: errors.New(`invalid use of field "authorization"`), errorLine: 3, errorPos: 5, reason: `setting "permissions" within cluster authorization block is deprecated`, }, { name: "when write deadline is used with deprecated usage", config: ` write_deadline = 100 `, warningErr: errors.New(`invalid use of field "write_deadline"`), errorLine: 2, errorPos: 17, reason: `write_deadline should be converted to a duration`, }, ///////////////////// // ACCOUNTS // ///////////////////// { name: "when accounts block is correctly configured", config: ` http_port = 8222 accounts { # # synadia > nats.io, cncf # synadia { # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL" users [ { # SUAEL6RU3BSDAFKOHNTEOK5Q6FTM5FTAMWVIKBET6FHPO4JRII3CYELVNM nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3" } ] exports = [ { service: "synadia.requests", accounts: [nats, cncf] } ] } # # nats < synadia # nats { # SUAJTM55JH4BNYDA22DMDZJSRBRKVDGSLYK2HDIOCM3LPWCDXIDV5Q4CIE nkey = "ADRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS" users [ { # SUADZTYQAKTY5NQM7XRB5XR3C24M6ROGZLBZ6P5HJJSSOFUGC5YXOOECOM nkey = "UD6AYQSOIN2IN5OGC6VQZCR4H3UFMIOXSW6NNS6N53CLJA4PB56CEJJI" } ] imports = [ # This account has to send requests to 'nats.requests' subject { service: { account: "synadia", subject: "synadia.requests" }, to: "nats.requests" } ] } # # cncf < synadia # cncf { # SAAFHDZX7SGZ2SWHPS22JRPPK5WX44NPLNXQHR5C5RIF6QRI3U65VFY6C4 nkey = "AD4YRVUJF2KASKPGRMNXTYKIYSCB3IHHB4Y2ME6B2PDIV5QJ23C2ZRIT" users [ { # SUAKINP3Z2BPUXWOFSW2FZC7TFJCMMU7DHKP2C62IJQUDASOCDSTDTRMJQ nkey = "UB57IEMPG4KOTPFV5A66QKE2HZ3XBXFHVRCCVMJEWKECMVN2HSH3VTSJ" } ] imports = [ # This account has to send requests to 'synadia.requests' subject { service: { account: "synadia", subject: "synadia.requests" } } ] } } `, err: nil, }, { name: "when nkey is invalid within accounts block", config: ` accounts { # # synadia > nats.io, cncf # synadia { # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL" users [ { # SUAEL6RU3BSDAFKOHNTEOK5Q6FTM5FTAMWVIKBET6FHPO4JRII3CYELVNM nkey = "SCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3" } ] exports = [ { service: "synadia.requests", accounts: [nats, cncf] } ] } # # nats < synadia # nats { # SUAJTM55JH4BNYDA22DMDZJSRBRKVDGSLYK2HDIOCM3LPWCDXIDV5Q4CIE nkey = "ADRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS" users [ { # SUADZTYQAKTY5NQM7XRB5XR3C24M6ROGZLBZ6P5HJJSSOFUGC5YXOOECOM nkey = "UD6AYQSOIN2IN5OGC6VQZCR4H3UFMIOXSW6NNS6N53CLJA4PB56CEJJI" } ] imports = [ # This account has to send requests to 'nats.requests' subject { service: { account: "synadia", subject: "synadia.requests" }, to: "nats.requests" } ] } # # cncf < synadia # cncf { # SAAFHDZX7SGZ2SWHPS22JRPPK5WX44NPLNXQHR5C5RIF6QRI3U65VFY6C4 nkey = "AD4YRVUJF2KASKPGRMNXTYKIYSCB3IHHB4Y2ME6B2PDIV5QJ23C2ZRIT" users [ { # SUAKINP3Z2BPUXWOFSW2FZC7TFJCMMU7DHKP2C62IJQUDASOCDSTDTRMJQ nkey = "UB57IEMPG4KOTPFV5A66QKE2HZ3XBXFHVRCCVMJEWKECMVN2HSH3VTSJ" } ] imports = [ # This account has to send requests to 'synadia.requests' subject { service: { account: "synadia", subject: "synadia.requests" } } ] } } `, err: errors.New(`Not a valid public nkey for a user`), errorLine: 14, errorPos: 11, }, { name: "when accounts block has unknown fields", config: ` http_port = 8222 accounts { foo = "bar" }`, err: errors.New(`Expected map entries for accounts`), errorLine: 5, errorPos: 19, }, { name: "when accounts has a referenced config variable within same block", config: ` accounts { PERMISSIONS = { publish = { allow = ["foo","bar"] deny = ["quux"] } } synadia { nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL" users [ { nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3" permissions = $PERMISSIONS } ] exports = [ { stream: "synadia.>" } ] } }`, err: nil, }, { name: "when accounts has an unreferenced config variables within same block", config: ` accounts { PERMISSIONS = { publish = { allow = ["foo","bar"] deny = ["quux"] } } synadia { nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL" users [ { nkey = "UCARKS2E3KVB7YORL2DG34XLT7PUCOL2SVM7YXV6ETHLW6Z46UUJ2VZ3" } ] exports = [ { stream: "synadia.>" } ] } }`, err: errors.New(`unknown field "publish"`), errorLine: 4, errorPos: 5, }, { name: "when accounts block defines a global account", config: ` http_port = 8222 accounts { $G = { } } `, err: errors.New(`"$G" is a Reserved Account`), errorLine: 5, errorPos: 19, }, { name: "when accounts block uses an invalid public key", config: ` accounts { synadia = { nkey = "invalid" } } `, err: errors.New(`Not a valid public nkey for an account: "invalid"`), errorLine: 4, errorPos: 21, }, { name: "when accounts list includes reserved account", config: ` port = 4222 accounts = [foo, bar, "$G"] http_port = 8222 `, err: errors.New(`"$G" is a Reserved Account`), errorLine: 4, errorPos: 26, }, { name: "when accounts list includes a dupe entry", config: ` port = 4222 accounts = [foo, bar, bar] http_port = 8222 `, err: errors.New(`Duplicate Account Entry: bar`), errorLine: 4, errorPos: 25, }, { name: "when accounts block includes a dupe user", config: ` port = 4222 accounts = { nats { users = [ { user: "foo", pass: "bar" }, { user: "hello", pass: "world" }, { user: "foo", pass: "bar" } ] } } http_port = 8222 `, err: errors.New(`Duplicate user "foo" detected`), errorLine: 6, errorPos: 21, }, { name: "when accounts block imports are not a list", config: ` port = 4222 accounts = { nats { imports = true } } http_port = 8222 `, err: errors.New(`Imports should be an array, got bool`), errorLine: 6, errorPos: 21, }, { name: "when accounts block exports are not a list", config: ` port = 4222 accounts = { nats { exports = true } } http_port = 8222 `, err: errors.New(`Exports should be an array, got bool`), errorLine: 6, errorPos: 21, }, { name: "when accounts block imports items are not a map", config: ` port = 4222 accounts = { nats { imports = [ false ] } } http_port = 8222 `, err: errors.New(`Import Items should be a map with type entry, got bool`), errorLine: 7, errorPos: 23, }, { name: "when accounts block export items are not a map", config: ` port = 4222 accounts = { nats { exports = [ false ] } } http_port = 8222 `, err: errors.New(`Export Items should be a map with type entry, got bool`), errorLine: 7, errorPos: 23, }, { name: "when accounts exports has a stream name that is not a string", config: ` port = 4222 accounts = { nats { exports = [ { stream: false } ] } } http_port = 8222 `, err: errors.New(`Expected stream name to be string, got bool`), errorLine: 8, errorPos: 25, }, { name: "when accounts exports has a service name that is not a string", config: ` accounts = { nats { exports = [ { service: false } ] } } `, err: errors.New(`Expected service name to be string, got bool`), errorLine: 6, errorPos: 25, }, { name: "when accounts imports stream without name", config: ` port = 4222 accounts = { nats { imports = [ { stream: { }} ] } } http_port = 8222 `, err: errors.New(`Expect an account name and a subject`), errorLine: 7, errorPos: 25, }, { name: "when accounts imports service without name", config: ` port = 4222 accounts = { nats { imports = [ { service: { }} ] } } http_port = 8222 `, err: errors.New(`Expect an account name and a subject`), errorLine: 7, errorPos: 25, }, { name: "when user authorization config has both token and users", config: ` authorization = { token = "s3cr3t" users = [ { user = "foo" pass = "bar" } ] } `, err: errors.New(`Can not have a token and a users array`), errorLine: 2, errorPos: 3, }, { name: "when user authorization config has both token and user", config: ` authorization = { user = "foo" pass = "bar" token = "baz" } `, err: errors.New(`Cannot have a user/pass and token`), errorLine: 2, errorPos: 3, }, { name: "when user authorization config has both user and users array", config: ` authorization = { user = "user1" pass = "pwd1" users = [ { user = "user2" pass = "pwd2" } ] } `, err: errors.New(`Can not have a single user/pass and a users array`), errorLine: 2, errorPos: 3, }, { name: "when user authorization has duplicate users", config: ` authorization = { users = [ {user: "user1", pass: "pwd"} {user: "user2", pass: "pwd"} {user: "user1", pass: "pwd"} ] } `, err: fmt.Errorf(`Duplicate user %q detected`, "user1"), errorLine: 2, errorPos: 3, }, { name: "when user authorization has duplicate nkeys", config: ` authorization = { users = [ {nkey: UC6NLCN7AS34YOJVCYD4PJ3QB7QGLYG5B5IMBT25VW5K4TNUJODM7BOX } {nkey: UBAAQWTW6CG2G6ANGNKB5U2B7HRWHSGMZEZX3AQSAJOQDAUGJD46LD2E } {nkey: UC6NLCN7AS34YOJVCYD4PJ3QB7QGLYG5B5IMBT25VW5K4TNUJODM7BOX } ] } `, err: fmt.Errorf(`Duplicate nkey %q detected`, "UC6NLCN7AS34YOJVCYD4PJ3QB7QGLYG5B5IMBT25VW5K4TNUJODM7BOX"), errorLine: 2, errorPos: 3, }, { name: "when user authorization config has users not as a list", config: ` authorization = { users = false } `, err: errors.New(`Expected users field to be an array, got false`), errorLine: 3, errorPos: 5, }, { name: "when user authorization config has users not as a map", config: ` authorization = { users = [false] } `, err: errors.New(`Expected user entry to be a map/struct, got false`), errorLine: 3, errorPos: 14, }, { name: "when user authorization config has permissions not as a map", config: ` authorization = { users = [{user: hello, pass: world}] permissions = false } `, err: errors.New(`Expected permissions to be a map/struct, got false`), errorLine: 4, errorPos: 19, }, { name: "when user authorization permissions config has invalid fields within allow", config: ` authorization { permissions { publish = { allow = [false, "hello", "world"] deny = ["foo", "bar"] } subscribe = {} } } `, err: errors.New(`Subject in permissions array cannot be cast to string`), errorLine: 5, errorPos: 18, }, { name: "when user authorization permissions config has invalid fields within deny", config: ` authorization { permissions { publish = { allow = ["hello", "world"] deny = [true, "foo", "bar"] } subscribe = {} } } `, err: errors.New(`Subject in permissions array cannot be cast to string`), errorLine: 6, errorPos: 17, }, { name: "when user authorization permissions config has invalid type", config: ` authorization { permissions { publish = { allow = false } subscribe = {} } } `, err: errors.New(`Expected subject permissions to be a subject, or array of subjects, got bool`), errorLine: 5, errorPos: 9, }, { name: "when user authorization permissions subject is invalid", config: ` authorization { permissions { publish = { allow = ["foo..bar"] } subscribe = {} } } `, err: errors.New(`subject "foo..bar" is not a valid subject`), errorLine: 5, errorPos: 9, }, { name: "when cluster config listen is invalid", config: ` cluster { listen = "0.0.0.0:XXXX" } `, err: errors.New(`could not parse port "XXXX"`), errorLine: 3, errorPos: 5, }, { name: "when cluster config includes multiple users", config: ` cluster { authorization { users = [] } } `, err: errors.New(`Cluster authorization does not allow multiple users`), errorLine: 3, errorPos: 5, }, { name: "when cluster routes are invalid", config: ` cluster { routes = [ "0.0.0.0:XXXX" # "0.0.0.0:YYYY" # "0.0.0.0:ZZZZ" ] } `, err: errors.New(`error parsing route url ["0.0.0.0:XXXX"]`), errorLine: 4, errorPos: 22, }, { name: "when setting invalid TLS config within cluster block", config: ` cluster { tls { } } `, err: nil, errorLine: 0, errorPos: 0, }, { name: "invalid lame_duck_duration type", config: ` lame_duck_duration: abc `, err: errors.New(`error parsing lame_duck_duration: time: invalid duration`), errorLine: 2, errorPos: 5, }, { name: "lame_duck_duration too small", config: ` lame_duck_duration: "5s" `, err: errors.New(`invalid lame_duck_duration of 5s, minimum is 30 seconds`), errorLine: 2, errorPos: 5, }, { name: "invalid lame_duck_grace_period type", config: ` lame_duck_grace_period: abc `, err: errors.New(`error parsing lame_duck_grace_period: time: invalid duration`), errorLine: 2, errorPos: 5, }, { name: "lame_duck_grace_period should be positive", config: ` lame_duck_grace_period: "-5s" `, err: errors.New(`invalid lame_duck_grace_period, needs to be positive`), errorLine: 2, errorPos: 5, }, { name: "when only setting TLS timeout for a leafnode remote", config: ` leafnodes { remotes = [ { url: "tls://nats:7422" tls { timeout: 0.01 } } ] }`, err: nil, errorLine: 0, errorPos: 0, }, { name: "verify_cert_and_check_known_urls do not work for leaf nodes", config: ` leafnodes { remotes = [ { url: "tls://nats:7422" tls { timeout: 0.01 verify_cert_and_check_known_urls: true } } ] }`, //Unexpected error after processing config: /var/folders/9h/6g_c9l6n6bb8gp331d_9y0_w0000gn/T/057996446:8:5: err: errors.New("verify_cert_and_check_known_urls not supported in this context"), errorLine: 8, errorPos: 5, }, { name: "when leafnode remotes use wrong type", config: ` leafnodes { remotes: { url: "tls://nats:7422" } }`, err: errors.New(`Expected remotes field to be an array, got map[string]interface {}`), errorLine: 3, errorPos: 5, }, { name: "when leafnode remotes url uses wrong type", config: ` leafnodes { remotes: [ { urls: 1234 } ] }`, err: errors.New(`Expected remote leafnode url to be an array or string, got 1234`), errorLine: 4, errorPos: 18, }, { name: "when leafnode min_version is wrong type", config: ` leafnodes { port: -1 min_version = 123 }`, err: errors.New(`interface conversion: interface {} is int64, not string`), errorLine: 4, errorPos: 6, }, { name: "when leafnode min_version has parsing error", config: ` leafnodes { port: -1 min_version = bad.version }`, err: errors.New(`invalid leafnode's minimum version: invalid semver`), errorLine: 4, errorPos: 6, }, { name: "when leafnode min_version is too low", config: ` leafnodes { port: -1 min_version = 2.7.9 }`, err: errors.New(`the minimum version should be at least 2.8.0`), errorLine: 4, errorPos: 6, }, { name: "when setting latency tracking with a system account", config: ` system_account: sys accounts { sys { users = [ {user: sys, pass: "" } ] } nats.io: { users = [ { user : bar, pass: "" } ] exports = [ { service: "nats.add" response: singleton latency: { sampling: 100% subject: "latency.tracking.add" } } ] } } `, err: nil, errorLine: 0, errorPos: 0, }, { name: "when setting latency tracking with an invalid publish subject", config: ` system_account = sys accounts { sys { users = [ {user: sys, pass: "" } ] } nats.io: { users = [ { user : bar, pass: "" } ] exports = [ { service: "nats.add" response: singleton latency: "*" } ] } } `, err: errors.New(`Error adding service latency sampling for "nats.add" on subject "*": invalid publish subject`), errorLine: 3, errorPos: 17, }, { name: "when setting latency tracking on a stream", config: ` system_account = sys accounts { sys { users = [ {user: sys, pass: "" } ] } nats.io: { users = [ { user : bar, pass: "" } ] exports = [ { stream: "nats.add" latency: "foo" } ] } } `, err: errors.New(`Detected latency directive on non-service`), errorLine: 11, errorPos: 25, }, { name: "when using duplicate service import subject", config: ` accounts { A: { users = [ {user: user1, pass: ""} ] exports = [ {service: "remote1"} {service: "remote2"} ] } B: { users = [ {user: user2, pass: ""} ] imports = [ {service: {account: "A", subject: "remote1"}, to: "local"} {service: {account: "A", subject: "remote2"}, to: "local"} ] } } `, err: errors.New(`Duplicate service import subject "local", previously used in import for account "A", subject "remote1"`), errorLine: 14, errorPos: 71, }, { name: "mixing single and multi users in leafnode authorization", config: ` leafnodes { authorization { user: user1 password: pwd users = [{user: user2, password: pwd}] } } `, err: errors.New("can not have a single user/pass and a users array"), errorLine: 3, errorPos: 20, }, { name: "duplicate usernames in leafnode authorization", config: ` leafnodes { authorization { users = [ {user: user, password: pwd} {user: user, password: pwd} ] } } `, err: errors.New(`duplicate user "user" detected in leafnode authorization`), errorLine: 3, errorPos: 21, }, { name: "mqtt bad type", config: ` mqtt [ "wrong" ] `, err: errors.New(`Expected mqtt to be a map, got []interface {}`), errorLine: 2, errorPos: 17, }, { name: "mqtt bad listen", config: ` mqtt { listen: "xxxxxxxx" } `, err: errors.New(`could not parse address string "xxxxxxxx"`), errorLine: 3, errorPos: 21, }, { name: "mqtt bad host", config: ` mqtt { host: 1234 } `, err: errors.New(`interface conversion: interface {} is int64, not string`), errorLine: 3, errorPos: 21, }, { name: "mqtt bad port", config: ` mqtt { port: "abc" } `, err: errors.New(`interface conversion: interface {} is string, not int64`), errorLine: 3, errorPos: 21, }, { name: "mqtt bad TLS", config: ` mqtt { port: -1 tls { cert_file: "./configs/certs/server.pem" } } `, err: errors.New(`missing 'key_file' in TLS configuration`), errorLine: 4, errorPos: 21, }, { name: "connection types wrong type", config: ` authorization { users [ {user: a, password: pwd, allowed_connection_types: 123} ] } `, err: errors.New(`error parsing allowed connection types: unsupported type int64`), errorLine: 4, errorPos: 53, }, { name: "connection types content wrong type", config: ` authorization { users [ {user: a, password: pwd, allowed_connection_types: [ 123 WEBSOCKET ]} ] } `, err: errors.New(`error parsing allowed connection types: unsupported type in array int64`), errorLine: 5, errorPos: 32, }, { name: "connection types type unknown", config: ` authorization { users [ {user: a, password: pwd, allowed_connection_types: [ "UNKNOWN" ]} ] } `, err: fmt.Errorf("invalid connection types [%q]", "UNKNOWN"), errorLine: 4, errorPos: 53, }, { name: "websocket auth unknown var", config: ` websocket { authorization { unknown: "field" } } `, err: fmt.Errorf("unknown field %q", "unknown"), errorLine: 4, errorPos: 25, }, { name: "websocket bad tls", config: ` websocket { tls { cert_file: "configs/certs/server.pem" } } `, err: fmt.Errorf("missing 'key_file' in TLS configuration"), errorLine: 3, errorPos: 21, }, { name: "verify_cert_and_check_known_urls not support for websockets", config: ` websocket { tls { cert_file: "configs/certs/server.pem" key_file: "configs/certs/key.pem" verify_cert_and_check_known_urls: true } } `, err: fmt.Errorf("verify_cert_and_check_known_urls not supported in this context"), errorLine: 6, errorPos: 10, }, { name: "ambiguous store dir", config: ` store_dir: "foo" jetstream { store_dir: "bar" } `, err: fmt.Errorf(`Duplicate 'store_dir' configuration`), }, { name: "token not supported in cluster", config: ` cluster { port: -1 authorization { token: "my_token" } } `, err: fmt.Errorf("Cluster authorization does not support tokens"), errorLine: 4, errorPos: 6, }, { name: "token not supported in gateway", config: ` gateway { port: -1 name: "A" authorization { token: "my_token" } } `, err: fmt.Errorf("Gateway authorization does not support tokens"), errorLine: 5, errorPos: 6, }, { name: "wrong type for cluter pool size", config: ` cluster { port: -1 pool_size: "abc" } `, err: fmt.Errorf("interface conversion: interface {} is string, not int64"), errorLine: 4, errorPos: 6, }, { name: "wrong type for cluter accounts", config: ` cluster { port: -1 accounts: 123 } `, err: fmt.Errorf("error parsing accounts: unsupported type int64"), errorLine: 4, errorPos: 6, }, { name: "wrong type for cluter compression", config: ` cluster { port: -1 compression: 123 } `, err: fmt.Errorf("field %q should be a boolean or a structure, got int64", "compression"), errorLine: 4, errorPos: 6, }, { name: "wrong type for cluter compression mode", config: ` cluster { port: -1 compression: { mode: 123 } } `, err: fmt.Errorf("interface conversion: interface {} is int64, not string"), errorLine: 5, errorPos: 7, }, { name: "wrong type for cluter compression rtt thresholds", config: ` cluster { port: -1 compression: { mode: "s2_auto" rtt_thresholds: 123 } } `, err: fmt.Errorf("interface conversion: interface {} is int64, not []interface {}"), errorLine: 6, errorPos: 7, }, { name: "invalid durations for cluter compression rtt thresholds", config: ` cluster { port: -1 compression: { mode: "s2_auto" rtt_thresholds: [abc] } } `, err: fmt.Errorf("time: invalid duration %q", "abc"), errorLine: 6, errorPos: 7, }, { name: "wrong type for leafnodes compression", config: ` leafnodes { port: -1 compression: 123 } `, err: fmt.Errorf("field %q should be a boolean or a structure, got int64", "compression"), errorLine: 4, errorPos: 6, }, { name: "wrong type for leafnodes compression mode", config: ` leafnodes { port: -1 compression: { mode: 123 } } `, err: fmt.Errorf("interface conversion: interface {} is int64, not string"), errorLine: 5, errorPos: 7, }, { name: "wrong type for leafnodes compression rtt thresholds", config: ` leafnodes { port: -1 compression: { mode: "s2_auto" rtt_thresholds: 123 } } `, err: fmt.Errorf("interface conversion: interface {} is int64, not []interface {}"), errorLine: 6, errorPos: 7, }, { name: "invalid durations for leafnodes compression rtt thresholds", config: ` leafnodes { port: -1 compression: { mode: "s2_auto" rtt_thresholds: [abc] } } `, err: fmt.Errorf("time: invalid duration %q", "abc"), errorLine: 6, errorPos: 7, }, { name: "wrong type for remote leafnodes compression", config: ` leafnodes { port: -1 remotes [ { url: "nats://127.0.0.1:123" compression: 123 } ] } `, err: fmt.Errorf("field %q should be a boolean or a structure, got int64", "compression"), errorLine: 7, errorPos: 8, }, { name: "wrong type for remote leafnodes compression mode", config: ` leafnodes { port: -1 remotes [ { url: "nats://127.0.0.1:123" compression: { mode: 123 } } ] } `, err: fmt.Errorf("interface conversion: interface {} is int64, not string"), errorLine: 8, errorPos: 9, }, { name: "wrong type for remote leafnodes compression rtt thresholds", config: ` leafnodes { port: -1 remotes [ { url: "nats://127.0.0.1:123" compression: { mode: "s2_auto" rtt_thresholds: 123 } } ] } `, err: fmt.Errorf("interface conversion: interface {} is int64, not []interface {}"), errorLine: 9, errorPos: 9, }, { name: "invalid durations for remote leafnodes compression rtt thresholds", config: ` leafnodes { port: -1 remotes [ { url: "nats://127.0.0.1:123" compression: { mode: "s2_auto" rtt_thresholds: [abc] } } ] } `, err: fmt.Errorf("time: invalid duration %q", "abc"), errorLine: 9, errorPos: 9, }, { name: "show warnings on empty configs without values", config: ``, warningErr: errors.New(`config has no values or is empty`), errorLine: 0, errorPos: 0, reason: "", }, { name: "show warnings on empty configs without values and only comments", config: `# Valid file but has no usable values. `, warningErr: errors.New(`config has no values or is empty`), errorLine: 0, errorPos: 0, reason: "", }, } checkConfig := func(config string) error { opts := &Options{ CheckConfig: true, } return opts.ProcessConfigFile(config) } checkErr := func(t *testing.T, err, expectedErr error) { t.Helper() switch { case err == nil && expectedErr == nil: // OK case err != nil && expectedErr == nil: t.Errorf("Unexpected error after processing config: %s", err) case err == nil && expectedErr != nil: t.Errorf("Expected %q error after processing invalid config but got nothing", expectedErr) } } for _, test := range tests { t.Run(test.name, func(t *testing.T) { conf := createConfFile(t, []byte(test.config)) err := checkConfig(conf) var expectedErr error // Check for either warnings or errors. if test.err != nil { expectedErr = test.err } else if test.warningErr != nil { expectedErr = test.warningErr } if err != nil && expectedErr != nil { var msg string if test.errorPos > 0 { msg = fmt.Sprintf("%s:%d:%d: %s", conf, test.errorLine, test.errorPos, expectedErr.Error()) if test.reason != "" { msg += ": " + test.reason } } else if test.warningErr != nil { msg = expectedErr.Error() } else { msg = test.reason } if !strings.Contains(err.Error(), msg) { t.Errorf("Expected:\n%q\ngot:\n%q", msg, err.Error()) } } checkErr(t, err, expectedErr) }) } } func TestConfigCheckIncludes(t *testing.T) { // Check happy path first. opts := &Options{ CheckConfig: true, } err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf") if err != nil { t.Errorf("Unexpected error processing include files with configuration check enabled: %v", err) } opts = &Options{ CheckConfig: true, } err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf") if err == nil { t.Errorf("Expected error processing include files with configuration check enabled: %v", err) } expectedErr := `include_bad_conf_check_b.conf:10:19: unknown field "monitoring_port"` + "\n" if err != nil && !strings.HasSuffix(err.Error(), expectedErr) { t.Errorf("Expected: \n%q, got\n: %q", expectedErr, err.Error()) } } func TestConfigCheckMultipleErrors(t *testing.T) { opts := &Options{ CheckConfig: true, } err := opts.ProcessConfigFile("./configs/multiple_errors.conf") if err == nil { t.Errorf("Expected error processing config files with multiple errors check enabled: %v", err) } cerr, ok := err.(*processConfigErr) if !ok { t.Fatalf("Expected a configuration process error") } got := len(cerr.Warnings()) expected := 1 if got != expected { t.Errorf("Expected a %d warning, got: %d", expected, got) } got = len(cerr.Errors()) // Could be 7 or 8 errors depending on internal ordering of the parsing. if got != 7 && got != 8 { t.Errorf("Expected 7 or 8 errors, got: %d", got) } errMsg := err.Error() errs := []string{ `./configs/multiple_errors.conf:12:1: invalid use of field "write_deadline": write_deadline should be converted to a duration`, `./configs/multiple_errors.conf:2:1: Cannot have a user/pass and token`, `./configs/multiple_errors.conf:10:1: unknown field "monitoring"`, `./configs/multiple_errors.conf:67:3: Cluster authorization does not allow multiple users`, `./configs/multiple_errors.conf:21:5: Not a valid public nkey for an account: "OC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL"`, `./configs/multiple_errors.conf:26:9: Not a valid public nkey for a user`, `./configs/multiple_errors.conf:36:5: Not a valid public nkey for an account: "ODRZ42QBM7SXQDXXTSVWT2WLLFYOQGAFC4TO6WOAXHEKQHIXR4HFYJDS"`, `./configs/multiple_errors.conf:41:9: Not a valid public nkey for a user`, } for _, msg := range errs { found := strings.Contains(errMsg, msg) if !found { t.Fatalf("Expected to find error %q", msg) } } if got == 8 { extra := "./configs/multiple_errors.conf:54:5: Can not have a single user/pass and accounts" if !strings.Contains(errMsg, extra) { t.Fatalf("Expected to find error %q (%s)", extra, errMsg) } } }