diff options
author | Antoine A <> | 2023-10-11 21:37:48 +0000 |
---|---|---|
committer | Antoine A <> | 2023-10-11 21:37:48 +0000 |
commit | 61f560d3624f30f78b90b9a804db7d5126e1a7da (patch) | |
tree | cf3ad0132b0fc2a8f70e996470171f127088c25e | |
parent | 9d43dc08ac94ff1f30d76ee3978ea2f4571c4369 (diff) | |
download | libeufin-61f560d3624f30f78b90b9a804db7d5126e1a7da.tar.gz libeufin-61f560d3624f30f78b90b9a804db7d5126e1a7da.tar.bz2 libeufin-61f560d3624f30f78b90b9a804db7d5126e1a7da.zip |
Support currency specification
-rw-r--r-- | bank/conf/test.conf | 17 | ||||
-rw-r--r-- | bank/conf/test_restrict.conf | 17 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt | 29 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt | 5 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 95 | ||||
-rw-r--r-- | bank/src/test/kotlin/Common.kt | 2 | ||||
-rw-r--r-- | bank/src/test/kotlin/LibeuFinApiTest.kt | 2 | ||||
-rw-r--r-- | bank/src/test/kotlin/TalerApiTest.kt | 4 | ||||
-rw-r--r-- | bank/src/test/kotlin/helpers.kt | 18 | ||||
-rw-r--r-- | util/src/main/kotlin/TalerConfig.kt | 122 | ||||
-rw-r--r-- | util/src/test/kotlin/TalerConfigTest.kt | 8 |
11 files changed, 176 insertions, 143 deletions
diff --git a/bank/conf/test.conf b/bank/conf/test.conf new file mode 100644 index 00000000..fe7450a6 --- /dev/null +++ b/bank/conf/test.conf @@ -0,0 +1,17 @@ +[libeufin-bank] +CURRENCY = KUDOS +DEFAULT_CUSTOMER_DEBT_LIMIT = KUDOS:100 +DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 +REGISTRATION_BONUS_ENABLED = NO +SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com + +[currency-kudos] +ENABLED = YES +name = "Kudos (Taler Demonstrator)" +code = "KUDOS" +decimal_separator = "," +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = NO +alt_unit_names = {"0":"ク"}
\ No newline at end of file diff --git a/bank/conf/test_restrict.conf b/bank/conf/test_restrict.conf new file mode 100644 index 00000000..462d94e6 --- /dev/null +++ b/bank/conf/test_restrict.conf @@ -0,0 +1,17 @@ +[libeufin-bank] +CURRENCY = KUDOS +DEFAULT_CUSTOMER_DEBT_LIMIT = KUDOS:100 +DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 +REGISTRATION_BONUS_ENABLED = NO +restrict_registration = YES + +[currency-kudos] +ENABLED = YES +name = "Kudos (Taler Demonstrator)" +code = "KUDOS" +decimal_separator = "," +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = NO +alt_unit_names = {"0":"ク"}
\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt index 67fe6a96..cefe8841 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt @@ -468,12 +468,14 @@ data class Cashout( // Type to return as GET /config response @Serializable // Never used to parse JSON. data class Config( - val name: String = "libeufin-bank", - val version: String = "0:0:0", - val have_cashout: Boolean = false, + val currency: CurrencySpecification, +) { + val name: String = "libeufin-bank" + val version: String = "0:0:0" + val have_cashout: Boolean = false // Following might probably get renamed: val fiat_currency: String? = null -) +} enum class CorebankCreditDebitInfo { credit, debit @@ -636,9 +638,22 @@ enum class BankTransactionResult { // GET /config response from the Taler Integration API. @Serializable data class TalerIntegrationConfigResponse( - val name: String = "taler-bank-integration", - val version: String = "0:0:0", - val currency: String + val currency: String, + val currency_specification: CurrencySpecification, +) { + val name: String = "taler-bank-integration"; + val version: String = "0:0:0"; +} + +@Serializable +data class CurrencySpecification( + val name: String, + val decimal_separator: String, + val num_fractional_input_digits: Int, + val num_fractional_normal_digits: Int, + val num_fractional_trailing_zero_digits: Int, + val is_currency_name_leading: Boolean, + val alt_unit_names: Map<String, String> ) /** diff --git a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt index b117a8d8..bd867c59 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt @@ -31,7 +31,10 @@ import tech.libeufin.util.stripIbanPayto fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) { get("/taler-integration/config") { val internalCurrency: String = ctx.currency - call.respond(TalerIntegrationConfigResponse(currency = internalCurrency)) + call.respond(TalerIntegrationConfigResponse( + currency = internalCurrency, + currency_specification = ctx.currencySpecification + )) return@get } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt index 8030a881..be706ef0 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -83,6 +83,7 @@ data class BankApplicationContext( * Main, internal currency of the bank. */ val currency: String, + val currencySpecification: CurrencySpecification, /** * Restrict account registration to the administrator. */ @@ -124,7 +125,43 @@ data class BankApplicationContext( * SPA is located. */ val spaCaptchaURL: String?, -) +) { + companion object { + /** + * Read the configuration of the bank from a config file. + * Throws an exception if the configuration is malformed. + */ + fun readFromConfig(cfg: TalerConfig): BankApplicationContext { + val currency = cfg.requireString("libeufin-bank", "currency") + val currencySpecification = cfg.sections.find { + it.startsWith("CURRENCY-") && cfg.requireBoolean(it, "enabled") && cfg.requireString(it, "code") == currency + }?.let { + CurrencySpecification( + name = cfg.requireString(it, "name"), + decimal_separator = cfg.requireString(it, "decimal_separator"), + num_fractional_input_digits = cfg.requireNumber(it, "fractional_input_digits"), + num_fractional_normal_digits = cfg.requireNumber(it, "fractional_normal_digits"), + num_fractional_trailing_zero_digits = cfg.requireNumber(it, "fractional_trailing_zero_digits"), + is_currency_name_leading = cfg.requireBoolean(it, "is_currency_name_leading"), + alt_unit_names = Json.decodeFromString(cfg.requireString(it, "alt_unit_names")) + ) + } ?: throw TalerConfigError("missing currency config for $currency") + return BankApplicationContext( + currency = currency, + restrictRegistration = cfg.lookupBoolean("libeufin-bank", "restrict_registration") ?: false, + cashoutCurrency = cfg.lookupString("libeufin-bank", "cashout_currency"), + defaultCustomerDebtLimit = cfg.requireAmount("libeufin-bank", "default_customer_debt_limit", currency), + registrationBonusEnabled = cfg.lookupBoolean("libeufin-bank", "registration_bonus_enabled") ?: false, + registrationBonus = cfg.requireAmount("libeufin-bank", "registration_bonus", currency), + suggestedWithdrawalExchange = cfg.lookupString("libeufin-bank", "suggested_withdrawal_exchange"), + defaultAdminDebtLimit = cfg.requireAmount("libeufin-bank", "default_admin_debt_limit", currency), + spaCaptchaURL = cfg.lookupString("libeufin-bank", "spa_captcha_url"), + restrictAccountDeletion = cfg.lookupBoolean("libeufin-bank", "restrict_account_deletion") ?: true, + currencySpecification = currencySpecification + ) + } + } +} /** @@ -404,7 +441,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankApplicationContext) { } routing { get("/config") { - call.respond(Config()) + call.respond(Config(ctx.currencySpecification)) return@get } this.accountsMgmtHandlers(db, ctx) @@ -471,15 +508,12 @@ fun durationFromPretty(s: String): Long { return durationUs } -fun TalerConfig.requireValueAmount(section: String, option: String, currency: String): TalerAmount { - val amountStr = lookupValueString(section, option) - if (amountStr == null) { +fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount { + val amountStr = lookupString(section, option) ?: throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") - } - val amount = parseTalerAmount2(amountStr, FracDigits.EIGHT) - if (amount == null) { + val amount = parseTalerAmount2(amountStr, FracDigits.EIGHT) ?: throw TalerConfigError("expected amount for section $section, option $option, but amount is malformed") - } + if (amount.currency != currency) { throw TalerConfigError( "expected amount for section $section, option $option, but currency is wrong (got ${amount.currency} expected $currency" @@ -488,27 +522,6 @@ fun TalerConfig.requireValueAmount(section: String, option: String, currency: St return amount } -/** - * Read the configuration of the bank from a config file. - * Throws an exception if the configuration is malformed. - */ -fun readBankApplicationContextFromConfig(cfg: TalerConfig): BankApplicationContext { - val currency = cfg.requireValueString("libeufin-bank", "currency") - return BankApplicationContext( - currency = currency, - restrictRegistration = cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_registration", false), - cashoutCurrency = cfg.lookupValueString("libeufin-bank", "cashout_currency"), - defaultCustomerDebtLimit = cfg.requireValueAmount("libeufin-bank", "default_customer_debt_limit", currency), - registrationBonusEnabled = cfg.lookupValueBooleanDefault("libeufin-bank", "registration_bonus_enabled", false), - registrationBonus = cfg.requireValueAmount("libeufin-bank", "registration_bonus", currency), - suggestedWithdrawalExchange = cfg.lookupValueString("libeufin-bank", "suggested_withdrawal_exchange"), - defaultAdminDebtLimit = cfg.requireValueAmount("libeufin-bank", "default_admin_debt_limit", currency), - spaCaptchaURL = cfg.lookupValueString("libeufin-bank", "spa_captcha_url"), - restrictAccountDeletion = cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_account_deletion", true) - ) -} - - class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = "dbinit") { private val configFile by option( "--config", "-c", @@ -529,8 +542,8 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = override fun run() { val config = TalerConfig(BANK_CONFIG_SOURCE) config.load(this.configFile) - val dbConnStr = config.requireValueString("libeufin-bankdb-postgres", "config") - val sqlDir = config.requireValuePath("libeufin-bankdb-postgres", "sql_dir") + val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") + val sqlDir = config.requirePath("libeufin-bankdb-postgres", "sql_dir") if (requestReset) { resetDatabaseTables(dbConnStr, sqlDir) } @@ -553,15 +566,15 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") override fun run() { val config = TalerConfig(BANK_CONFIG_SOURCE) config.load(this.configFile) - val ctx = readBankApplicationContextFromConfig(config) - val dbConnStr = config.requireValueString("libeufin-bankdb-postgres", "config") + val ctx = BankApplicationContext.readFromConfig(config) + val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") logger.info("using database '$dbConnStr'") - val serveMethod = config.requireValueString("libeufin-bank", "serve") + val serveMethod = config.requireString("libeufin-bank", "serve") if (serveMethod.lowercase() != "tcp") { logger.info("Can only serve libeufin-bank via TCP") exitProcess(1) } - val servePortLong = config.requireValueNumber("libeufin-bank", "port") + val servePortLong = config.requireNumber("libeufin-bank", "port") val servePort = servePortLong.toInt() val db = Database(dbConnStr, ctx.currency) if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper @@ -589,9 +602,9 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { override fun run() { val config = TalerConfig(BANK_CONFIG_SOURCE) config.load(this.configFile) - val ctx = readBankApplicationContextFromConfig(config) - val dbConnStr = config.requireValueString("libeufin-bankdb-postgres", "config") - config.requireValueNumber("libeufin-bank", "port") + val ctx = BankApplicationContext.readFromConfig(config) + val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") + config.requireNumber("libeufin-bank", "port") val db = Database(dbConnStr, ctx.currency) if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) @@ -671,14 +684,14 @@ class BankConfigGet : CliktCommand("Lookup config value", name = "get") { val config = TalerConfig(BANK_CONFIG_SOURCE) config.load(this.configFile) if (isPath) { - val res = config.lookupValuePath(sectionName, optionName) + val res = config.lookupPath(sectionName, optionName) if (res == null) { logger.info("value not found in config") exitProcess(2) } println(res) } else { - val res = config.lookupValueString(sectionName, optionName) + val res = config.lookupString(sectionName, optionName) if (res == null) { logger.info("value not found in config") exitProcess(2) diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt index 7bb6e518..02e4327d 100644 --- a/bank/src/test/kotlin/Common.kt +++ b/bank/src/test/kotlin/Common.kt @@ -28,7 +28,7 @@ fun initDb(): Database { // We assume that libeufin-bank is installed. We could also try to locate the source tree here. val config = TalerConfig(ConfigSource("libeufin-bank", "libeufin-bank")) config.load() - val sqlPath = config.requireValuePath("libeufin-bankdb-postgres", "SQL_DIR") + val sqlPath = config.requirePath("libeufin-bankdb-postgres", "SQL_DIR") val dbConnStr = "postgresql:///libeufincheck" resetDatabaseTables(dbConnStr, sqlPath) initializeDatabaseTables(dbConnStr, sqlPath) diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt index 5cb98a3b..d50e3f99 100644 --- a/bank/src/test/kotlin/LibeuFinApiTest.kt +++ b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -569,7 +569,7 @@ class LibeuFinApiTest { * Test admin-only account creation */ @Test - fun createAccountRestrictedTest() = setup(restrictRegistration = true) { db, ctx -> + fun createAccountRestrictedTest() = setup(conf = "test_restrict.conf") { db, ctx -> testApplication { application { corebankWebApp(db, ctx) diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt index 308355ac..0d645c08 100644 --- a/bank/src/test/kotlin/TalerApiTest.kt +++ b/bank/src/test/kotlin/TalerApiTest.kt @@ -543,7 +543,7 @@ class TalerApiTest { } // Selecting withdrawal details from the Integration API endpoint. @Test - fun intSelect() = setup(suggestedExchange = "payto://iban/ABC123") { db, ctx -> + fun intSelect() = setup { db, ctx -> val uuid = UUID.randomUUID() assertNotNull(db.customerCreate(customerFoo)) assertNotNull(db.bankAccountCreate(bankAccountFoo)) @@ -568,7 +568,7 @@ class TalerApiTest { } // Showing withdrawal details from the Integrtion API endpoint. @Test - fun intGet() = setup(suggestedExchange = "payto://iban/ABC123") { db, ctx -> + fun intGet() = setup { db, ctx -> val uuid = UUID.randomUUID() assert(db.customerCreate(customerFoo) != null) assert(db.bankAccountCreate(bankAccountFoo) != null) diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt index 413d1e45..fa10e05d 100644 --- a/bank/src/test/kotlin/helpers.kt +++ b/bank/src/test/kotlin/helpers.kt @@ -17,23 +17,13 @@ fun setupDb(lambda: (Database) -> Unit) { } fun setup( - restrictRegistration: Boolean = false, - suggestedExchange: String = "https://exchange.example.com", + conf: String = "test.conf", lambda: (Database, BankApplicationContext) -> Unit ){ val db = initDb() - val ctx = BankApplicationContext( - currency = "KUDOS", - restrictRegistration = restrictRegistration, - cashoutCurrency = "EUR", - defaultCustomerDebtLimit = TalerAmount(100, 0, "KUDOS"), - defaultAdminDebtLimit = TalerAmount(10000, 0, "KUDOS"), - registrationBonusEnabled = false, - registrationBonus = null, - suggestedWithdrawalExchange = suggestedExchange, - spaCaptchaURL = null, - restrictAccountDeletion = true - ) + val config = TalerConfig(BANK_CONFIG_SOURCE) + config.load("conf/$conf") + val ctx = BankApplicationContext.readFromConfig(config) db.use { lambda(db, ctx) } diff --git a/util/src/main/kotlin/TalerConfig.kt b/util/src/main/kotlin/TalerConfig.kt index a8cf0ae0..684a8a1b 100644 --- a/util/src/main/kotlin/TalerConfig.kt +++ b/util/src/main/kotlin/TalerConfig.kt @@ -23,10 +23,8 @@ import kotlin.io.path.Path import kotlin.io.path.isReadable import kotlin.io.path.listDirectoryEntries -private data class Entry(val value: String) - private data class Section( - val entries: MutableMap<String, Entry>, + val entries: MutableMap<String, String>, ) private val reEmptyLine = Regex("^\\s*$") @@ -57,6 +55,7 @@ class TalerConfig( private val componentName = configSource.componentName private val installPathBinary = configSource.installPathBinary + val sections: Set<String> get() = sectionMap.keys private fun internalLoadFromString(s: String, sourceFilename: String?) { val lines = s.lines() @@ -125,9 +124,7 @@ class TalerConfig( optVal = optVal.substring(1, optVal.length - 1) } val section = provideSection(currentSection) - section.entries[optName] = Entry( - value = optVal - ) + section.entries[optName] = optVal continue } throw TalerConfigError("expected section header, option assignment or directive in line $lineNum file ${sourceFilename ?: "<input>"}") @@ -187,77 +184,15 @@ class TalerConfig( internalLoadFromString(s, null) } - private fun lookupEntry(section: String, option: String): Entry? { - val canonSection = section.uppercase() - val canonOption = option.uppercase() - return this.sectionMap[canonSection]?.entries?.get(canonOption) - } - - /** - * Look up a string value from the configuration. - * - * Return null if the value was not found in the configuration. - */ - fun lookupValueString(section: String, option: String): String? { - return lookupEntry(section, option)?.value - } - - fun requireValueString(section: String, option: String): String { - val entry = lookupEntry(section, option) - if (entry == null) { - throw TalerConfigError("expected string in configuration section $section option $option") - } - return entry.value - } - - fun requireValueNumber(section: String, option: String): Long { - val entry = lookupEntry(section, option) - if (entry == null) { - throw TalerConfigError("expected string in configuration section $section option $option") - } - return entry.value.toLong(10) - } - - fun lookupValueBooleanDefault(section: String, option: String, default: Boolean): Boolean { - val entry = lookupEntry(section, option) - if (entry == null) { - return default - } - val v = entry.value.lowercase() - if (v == "yes") { - return true; - } - if (v == "false") { - return false; - } - throw TalerConfigError("expected yes/no in configuration section $section option $option but got $v") - } - private fun setSystemDefault(section: String, option: String, value: String) { // FIXME: The value should be marked as a system default for diagnostics pretty printing val sec = provideSection(section) - sec.entries[option.uppercase()] = Entry(value = value) + sec.entries[option.uppercase()] = value } fun putValueString(section: String, option: String, value: String) { val sec = provideSection(section) - sec.entries[option.uppercase()] = Entry(value = value) - } - - fun lookupValuePath(section: String, option: String): String? { - val entry = lookupEntry(section, option) - if (entry == null) { - return null - } - return pathsub(entry.value) - } - - fun requireValuePath(section: String, option: String): String { - val res = lookupValuePath(section, option) - if (res == null) { - throw TalerConfigError("expected path for section $section option $option") - } - return res + sec.entries[option.uppercase()] = value } /** @@ -272,7 +207,7 @@ class TalerConfig( outStr.appendLine("[$sectionName]") headerWritten = true } - outStr.appendLine("$optionName = ${entry.value}") + outStr.appendLine("$optionName = $entry") } if (headerWritten) { outStr.appendLine() @@ -316,7 +251,7 @@ class TalerConfig( } private fun variableLookup(x: String, recursionDepth: Int = 0): String? { - val pathRes = this.lookupValueString("PATHS", x) + val pathRes = this.lookupString("PATHS", x) if (pathRes != null) { return pathsub(pathRes, recursionDepth + 1) } @@ -483,4 +418,47 @@ class TalerConfig( } return "/usr" } + + /* ----- Lookup ----- */ + + /** + * Look up a string value from the configuration. + * + * Return null if the value was not found in the configuration. + */ + fun lookupString(section: String, option: String): String? { + val canonSection = section.uppercase() + val canonOption = option.uppercase() + return this.sectionMap[canonSection]?.entries?.get(canonOption) + } + + fun requireString(section: String, option: String): String = + lookupString(section, option) ?: + throw TalerConfigError("expected string in configuration section $section option $option") + + fun requireNumber(section: String, option: String): Int = + lookupString(section, option)?.toInt() ?: + throw TalerConfigError("expected number in configuration section $section option $option") + + fun lookupBoolean(section: String, option: String): Boolean? { + val entry = lookupString(section, option) ?: return null + return when (val v = entry.lowercase()) { + "yes" -> true + "no" -> false + else -> throw TalerConfigError("expected yes/no in configuration section $section option $option but got $v") + } + } + + fun requireBoolean(section: String, option: String): Boolean = + lookupBoolean(section, option) ?: + throw TalerConfigError("expected boolean in configuration section $section option $option") + + fun lookupPath(section: String, option: String): String? { + val entry = lookupString(section, option) ?: return null + return pathsub(entry) + } + + fun requirePath(section: String, option: String): String = + lookupPath(section, option) ?: + throw TalerConfigError("expected path for section $section option $option") } diff --git a/util/src/test/kotlin/TalerConfigTest.kt b/util/src/test/kotlin/TalerConfigTest.kt index f07edede..4be47948 100644 --- a/util/src/test/kotlin/TalerConfigTest.kt +++ b/util/src/test/kotlin/TalerConfigTest.kt @@ -39,7 +39,7 @@ class TalerConfigTest { println(conf.stringify()) - assertEquals("baz", conf.lookupValueString("foo", "bar")) + assertEquals("baz", conf.lookupString("foo", "bar")) println(conf.getInstallPath()) } @@ -52,11 +52,11 @@ class TalerConfigTest { conf.putValueString("foo", "bar", "baz") conf.putValueString("foo", "bar2", "baz") - assertEquals("baz", conf.lookupValueString("foo", "bar")) - assertEquals("baz", conf.lookupValuePath("foo", "bar")) + assertEquals("baz", conf.lookupString("foo", "bar")) + assertEquals("baz", conf.lookupPath("foo", "bar")) conf.putValueString("foo", "dir1", "foo/\$DATADIR/bar") - assertEquals("foo/mydir/bar", conf.lookupValuePath("foo", "dir1")) + assertEquals("foo/mydir/bar", conf.lookupPath("foo", "dir1")) } } |