diff --git a/pycapo/_version.py b/pycapo/_version.py
index 61f2905c27af849c44c2823d8f4b94d2b681e778..09d340ed7a4a399169a1a6bdf698ca028c4c450f 100644
--- a/pycapo/_version.py
+++ b/pycapo/_version.py
@@ -1,2 +1,2 @@
 """ Version information for this package, don't put anything else here. """
-___version___ = '0.2.1'
+___version___ = '0.3.1'
diff --git a/pycapo/models.py b/pycapo/models.py
index 158a21da000f56ea9f711b48b81e14d74d8eb503..c0382ff6d75d3afa57ee732f10ded6093b53fcbe 100644
--- a/pycapo/models.py
+++ b/pycapo/models.py
@@ -2,7 +2,6 @@
 import os
 import os.path
 import re
-import sys
 
 try:
     import configparser
@@ -16,7 +15,7 @@ except ImportError:
 
 from pycapo import DEFAULT_CAPO_PATH
 
-_ENV_PATTERN = re.compile('\$\{env:([a-zA-Z\_]+)\}')
+_ENV_PATTERN = re.compile(r'\$\{env:([a-zA-Z\_]+)\}')
 
 
 class CapoConfig:
@@ -262,9 +261,10 @@ class _SimpleConfigParser(configparser.RawConfigParser):
     NOSECTION = 'NOSECTION'
 
     def read(self, filename):
-        text = open(filename, 'r').read()
-        f = stringio.StringIO("[%s]\n" % self.NOSECTION + text)
-        self.readfp(f, filename)
+        with open(filename, 'r') as f:
+            text = f.read()
+            f = stringio.StringIO("[%s]\n" % self.NOSECTION + text)
+            self.read_file(f, filename)
 
     def getoption(self, option):
         'get the value of an option'
diff --git a/setup.py b/setup.py
index 5391dd1899da35c373a40e6691e126116ea22451..2a1ff5fa62c50dfd6fd628282d8c61d5a8d3c65e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 
-from setuptools import setup, find_packages
+''' CAPO setup '''
+
 import os
+from setuptools import setup, find_packages
 
 NAME = 'pycapo'
 HERE = os.path.abspath(os.path.dirname(__file__))
@@ -18,6 +20,8 @@ setup(name=NAME,
       long_description=README + '\n\n' + CHANGES,
       author='Stephan Witz',
       author_email='switz@nrao.edu',
+      maintainer='Janet L. Goldstein',
+      maintainer_email='jgoldste@nrao.edu',
       url='https://open-bitbucket.nrao.edu/projects/SSA/repos/pycapo',
       keywords='',
       license='GPL',
@@ -43,7 +47,7 @@ setup(name=NAME,
       test_suite='pycapo.tests',
       install_requires=[],
       tests_require=['pytest'],
-      setup_requires=['pytest-runner','pytest'],
+      setup_requires=['pytest-runner', 'pytest'],
       entry_points={
           'console_scripts': [
               'pycapo = pycapo.commands:pycapo'
diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py
new file mode 100755
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/test_data/dev_profile.properties b/tests/test_data/dev_profile.properties
new file mode 100755
index 0000000000000000000000000000000000000000..7875987a37448ae8cf9822c01153ca79ca31897f
--- /dev/null
+++ b/tests/test_data/dev_profile.properties
@@ -0,0 +1,11 @@
+# PyCapo configuration file to set values for development context.
+# Ensure that the environment variable $CAPO_PROFILE is set correctly
+# to enable use of this file.
+
+tunes.looney.metadataDatabaseJdbcUsername = elmer
+tunes.looney.metadataDatabaseJdbcPassword = wascally_wabbit
+tunes.looney.metadataDatabaseJdbcUrl = jdbc:postgresql://dev.looney-toons.net/meta
+
+tunes.looney.someOtherDatabaseJdbcUsername = bugs_ro
+tunes.looney.someOtherDatabaseJdbcPassword = 'wazzup-doc'
+tunes.looney.someOtherDatabaseJdbcUrl = jdbc:oracle:thin:@//dev.looney-toons.net:1521/legacy
diff --git a/tests/test_data/empty_profile.properties b/tests/test_data/empty_profile.properties
new file mode 100755
index 0000000000000000000000000000000000000000..cbfb60d43ef877189f01a33a136c3103d44c35f7
--- /dev/null
+++ b/tests/test_data/empty_profile.properties
@@ -0,0 +1 @@
+tunes.looney.metadataDatabaseJdbcUsername =
diff --git a/tests/test_data/prod_profile.properties b/tests/test_data/prod_profile.properties
new file mode 100755
index 0000000000000000000000000000000000000000..55d6d65a50e726f8c91971a7e79791678f583c87
--- /dev/null
+++ b/tests/test_data/prod_profile.properties
@@ -0,0 +1,11 @@
+# PyCapo configuration file to set values for production context.
+# Ensure that the environment variable $CAPO_PROFILE is set correctly
+# to enable use of this file.
+
+tunes.looney.metadataDatabaseJdbcUsername = elmer
+tunes.looney.metadataDatabaseJdbcPassword = wascally_wabbit
+tunes.looney.metadataDatabaseJdbcUrl = jdbc:postgresql://prod.looney-toons.net/meta
+
+tunes.looney.someOtherDatabaseJdbcUsername = bugs
+tunes.looney.someOtherDatabaseJdbcPassword = 'no, really'
+tunes.looney.someOtherDatabaseJdbcUrl = jdbc:oracle:thin:@//prod.looney-toons.net:1521/legacy
diff --git a/tests/test_properties.py b/tests/test_properties.py
index c001f2dd19732fd133dfde14490d231074a01497..2dca14cc5dd46eed0409c0d3a4ba161ae4328bec 100644
--- a/tests/test_properties.py
+++ b/tests/test_properties.py
@@ -27,7 +27,7 @@ OPTIONS = (
 class CapoTest:
     """ A class that builds a test environment for pycapo with two
     properties files, a/test.properties and b/test.properties, with
-    options defined in the OPTOINS dict above. """
+    options defined in the OPTIONS dict above. """
 
     def __init__(self, tmpdir):
         self.tmpdir = tmpdir
diff --git a/tests/test_pycapo.py b/tests/test_pycapo.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b38ad10e11c343274e75d05b28fb0e04503e47d
--- /dev/null
+++ b/tests/test_pycapo.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+
+""" CapoConfig tests """
+
+import os
+import shutil
+import subprocess
+import sys
+import unittest
+from enum import Enum
+from pathlib import Path
+
+import pytest
+
+from pycapo import CapoConfig
+
+_TEST_PROFILES = ['dev_profile', 'prod_profile', 'empty_profile']
+
+class Keys(Enum):
+    ''' Keys in our fake Capo profiles '''
+    METADATA_USER = 'TUNES.LOONEY.METADATADATABASE.JDBCUSERNAME'
+    METADATA_PW   = 'TUNES.LOONEY.METADATADATABASEJDBCPASSWORD'
+    METADATA_URL  = 'TUNES.LOONEY.METADATADATABASEJDBCURL'
+    OTHER_USER    = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCUSERNAME'
+    OTHER_PW      = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCPASSWORD'
+    OTHER_URL     = 'TUNES.LOONEY.SOMEOTHERDATABASEJDBCURL'
+
+class PycapoTestCase(unittest.TestCase):
+    ''' Tests for pycapo and CapoConfig() '''
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.get_props_files(cls)
+        cls.capo_dir_was_created = False
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        cls.delete_properties(cls)
+
+    def test_capo_copes_with_bad_path(self):
+        ''' if capo path is bad, capo should default to something usual
+            rather than crashing
+        '''
+        user = os.environ['USER']
+        for profile in _TEST_PROFILES:
+            capo_config = CapoConfig(profile=profile, path='foo')
+            self.assertTrue(user in capo_config.getpath())
+            try:
+                for key, val in capo_config.getoptions().items():
+                    if 'prod' not in profile:
+                        self.assertFalse('prod' in val)
+                        if key == Keys.OTHER_URL.value:
+                            self.assertTrue('dev' in val)
+                    else:
+                        if key == Keys.METADATA_USER.value:
+                            self.assertTrue('_ro' not in val)
+                        if key == Keys.OTHER_URL.value:
+                            self.assertTrue('prod' in val)
+            except Exception as exc:
+                pytest.fail(f'failure for {capo_config.profile}, '
+                            f'{capo_config.getpath()}: {exc}')
+
+    def test_gets_expected_values_for_profile(self):
+        ''' properties files for different profiles may (or may not)
+            have disparate values
+        '''
+        for profile in _TEST_PROFILES:
+
+            try:
+                capo_config = CapoConfig(profile=profile)
+
+                try:
+                    options = capo_config.getoptions()
+                    for key, val in options.items():
+                        if key == Keys.OTHER_URL:
+                            self.assertTrue('thin' in val)
+                        if 'prod' not in profile:
+                            self.assertFalse('prod' in val)
+                            if key == Keys.OTHER_URL.value:
+                                self.assertTrue('dev' in val)
+                        else:
+                            if key == Keys.METADATA_USER.value:
+                                self.assertTrue('_ro' not in val)
+                            if key == Keys.OTHER_URL.value:
+                                self.assertTrue('prod' in val)
+                except Exception as exc:
+                    pytest.fail(f'failure for {profile} '
+                                f'when {key}=={val}: {exc}')
+
+            except Exception as exc:
+                pytest.fail(f'failure for {profile}: {exc}')
+
+    def test_capo_config_handles_bad_args_expectedly(self):
+        ''' CapoConfig() should complain when given bad args or none '''
+        with pytest.raises(ValueError):
+            CapoConfig(profile=None)
+            CapoConfig()
+            CapoConfig(path='foo')
+
+        bogus_config = CapoConfig(profile='bogus')
+        # CapoConfig() should NOT have any default values
+        self.assertEqual(0, len(bogus_config.getoptions()))
+        self.assertEqual(0, len(bogus_config.getlocations()))
+
+    def test_missing_setting_fails_expectedly(self):
+        ''' missing Capo setting should return appropriate code '''
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(profile='empty_profile')
+            self.assertEqual(3, exc.value)
+
+
+    def test_command_line_returns_expected_code(self):
+        ''' under various scenarios, pycapo should return appropriate code
+            for each
+        '''
+
+        # no arguments: should fail w/approp return code
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run()
+            self.assertEqual(2, exc.value)
+        # one argument, not a k/v pair: should fail w/approp return code
+        with pytest.raises(TypeError) as exc:
+            CommandLineLauncher().run('dev_profile')
+            self.assertEqual(2, exc.value)
+        # invalid k/v pair
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(foo='bar')
+            self.assertEqual(1, exc.value)
+        # key but no value
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(profile=None)
+            self.assertEqual(1, exc.value)
+        # invalid profile
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(profile='foo')
+            self.assertEqual(1, exc.value)
+        # no profile; invalid path
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(path='foo')
+            self.assertEqual(1, exc.value)
+        # no profile; valid path
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(path=Path.cwd())
+            self.assertEqual(1, exc.value)
+        # valid profile, invalid path
+        with pytest.raises(SystemExit) as exc:
+            CommandLineLauncher().run(profile='dev_profile', path='foo')
+            self.assertEqual(1, exc.value)
+
+    ### UTILITIES ###
+
+    def get_props_files(self):
+        ''' grab our fake properties files  and copy them to user's .capo dir '''
+        user_home = Path(os.environ['HOME'])
+        capo_path = user_home / '.capo'
+        props_source_dir = Path.cwd() / 'tests/test_data'
+
+        if not capo_path.is_dir():
+            capo_path.mkdir()
+            self.capo_dir_was_created = True
+        for profile in _TEST_PROFILES:
+            filename = profile + '.properties'
+            source = props_source_dir / filename
+            destn = capo_path / filename
+            shutil.copy(str(source), str(destn))
+
+
+    def delete_properties(self):
+        ''' delete fake properties files from user's .capo dir '''
+        user_home = Path(os.environ['HOME'])
+        default_path = user_home / '.capo'
+        for profile in _TEST_PROFILES:
+            filename = profile + '.properties'
+            to_delete = default_path / filename
+            if to_delete.is_file():
+                to_delete.unlink()
+        if self.capo_dir_was_created:
+            default_path.unlink()
+
+
+class CommandLineLauncher:
+    ''' launches a system process and executes a pycapo command '''
+
+    def run(self, **kwargs):
+        ''' kick off pycapo with these args and grab return code '''
+        args = ['pycapo']
+        if kwargs:
+            for arg in kwargs:
+                args.append(arg)
+
+        try:
+            proc = subprocess.run(args,
+                                  stdout=subprocess.PIPE,
+                                  stderr=subprocess.STDOUT,
+                                  check=False,
+                                  timeout=100,
+                                  bufsize=1,
+                                  universal_newlines=True)
+            if proc.returncode != 0:
+                sys.exit(proc.returncode)
+            return proc.returncode
+
+        except Exception as exc:
+            pytest.fail(f'Error launching pycapo with args {args}: {exc}')
+
+
+if __name__ == '__main__':
+    unittest.main()