Complete adapter tests. Check in a sample config.
diff --git a/Makefile b/Makefile
index 364ac8c..ef8654f 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,13 @@
 test:
 	bazel test ... --test_output=all
 
+clean:
+	bazel clean
+
 checkfmt:
 	(cd adapter; ../tools/checkfmt.sh)
 	(cd common; ../tools/checkfmt.sh)
+	(cd cmd/mockserver; ../../tools/checkfmt.sh)
 	(cd mock; ../tools/checkfmt.sh)
 
 presubmit: checkfmt test
diff --git a/README.md b/README.md
index 86cfed4..851eb64 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,46 @@
 # Istio Apigee Adapter
 
-This workspace holds an apigee adapter for Istio.
+This workspace holds an apigee adapter for Istio. It can be tested by itself, but in order
+to really use it you need a build of the Istio mixer that pulls it in. Instructions for that
+are forthcoming.
 
-## Installation
+## Building
 
-In order to use this, right now, you need a build of Istio
\ No newline at end of file
+You need Bazel, just like with Istio.
+
+    make
+
+Builds everything, and
+
+    make test
+
+Runs the tests.
+
+## Launching with the mixer
+
+Assuming that you have built a Mixer that includes it, you can launch the mixer as follows:
+
+First, edit testdata/global/adapters.yml to specify the organization and environment name for
+your Apigee adapter to use. If you deployed "edgemicro-auth" to a production org, then
+that is all you need. Otherwise, you can use "validationURL" to specify the base path
+to another proxy.
+
+Now, launch the Mixer binary:
+
+    $MIXER_HOME/bazel-bin/cmd/server/mixs --logtostderr --configStoreURL fs://$THIS_ROOT/testdata/configroot
+
+(In other words the "config store URL" needs to be the absolute path of the "testdata/configroot"
+directory of this repo. If you get this wrong then the mixer will silently do nothing
+and always return "OK!")
+
+Once that's running, here's a sample command:
+
+    $MIXER_HOME/bazel-bin/cmd/client/mixc check -a target.serivce=fault.svc.cluster.local \
+    --stringmap_attributes request.headers=apikey:SOME_VALID_API_KEY
+
+That will send a "check" RPC to the mixer, which will respond "OK" if and only if the
+API key is valid.
+
+You can also run "bazel-bin/cmd/mockserver," which will implement the same API as
+edgemicro-auth, but it will do it locally so that you can test easily. With the mock
+server, "12345" is a valid API key.
diff --git a/adapter/BUILD b/adapter/BUILD
index 0379f87..9e57dc2 100644
--- a/adapter/BUILD
+++ b/adapter/BUILD
@@ -11,15 +11,18 @@
     ],
     deps = [
         "//adapter/config:go_default_library",
+        "//common:go_default_library",
         "//external:mixer_adapter",
         "@com_github_hashicorp_go_multierror//:go_default_library",
     ],
 )
 
 go_test(
-    name = "small_tests",
-    size = "small",
+    name = "tests",
+    size = "medium",
     srcs = ["apigeeKeyChecker_test.go"],
     library = ":go_default_library",
-    deps = [],
+    deps = [
+        "//mock:go_default_library",
+    ],
 )
diff --git a/adapter/apigee.go b/adapter/apigee.go
index 8bf7f7d..c11818e 100644
--- a/adapter/apigee.go
+++ b/adapter/apigee.go
@@ -14,7 +14,7 @@
 limitations under the License.
 */
 
-package apigee
+package adapter
 
 import (
 	"istio.io/mixer/pkg/adapter"
diff --git a/adapter/apigeeKeyChecker.go b/adapter/apigeeKeyChecker.go
index 52d5010..6785f6f 100644
--- a/adapter/apigeeKeyChecker.go
+++ b/adapter/apigeeKeyChecker.go
@@ -14,21 +14,24 @@
 limitations under the License.
 */
 
-package apigee
+package adapter
 
 import (
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"net/url"
 
 	"github.com/apid/istioApigeeAdapter/adapter/config"
+	"github.com/apid/istioApigeeAdapter/common"
 	"istio.io/mixer/pkg/adapter"
 )
 
 const (
-	checkName = "apigeeKeyChecker"
-	checkDesc = "Verify an API key from a parameter"
+	checkName     = "apigeeKeyChecker"
+	checkDesc     = "Verify an API key from a parameter"
+	verifyKeyPath = "/verifyApiKey"
 )
 
 var checkConf = &config.VerifyKeyParams{}
@@ -38,31 +41,51 @@
 }
 
 type keyChecker struct {
-	keyParam     string
-	organization string
-	environment  string
+	env      adapter.Env
+	checkURL string
 }
 
-type APIKeyBody struct {
-	APIKey string `json:"apiKey"`
-}
-
-func newKeyCheckBuilder() keyCheckBuilder {
+func newKeyCheckBuilder() adapter.ListsBuilder {
 	return keyCheckBuilder{
 		adapter.NewDefaultBuilder(checkName, checkDesc, checkConf),
 	}
 }
 
-func (keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
-	return newKeyChecker(c.(*config.VerifyKeyParams))
+func (b keyCheckBuilder) ValidateConfig(c adapter.Config) (ce *adapter.ConfigErrors) {
+	cfg := c.(*config.VerifyKeyParams)
+	if cfg.Organization == "" {
+		ce = ce.Appendf("organization", "Organization parameter must be specified")
+	}
+	if cfg.Environment == "" {
+		ce = ce.Appendf("environment", "Environment parameter must be specified")
+	}
+	if cfg.VerificationURL != "" {
+		_, err := url.Parse(cfg.VerificationURL)
+		if err != nil {
+			ce = ce.Appendf("verificationURL", "Invalid verification URL: %s", err)
+		}
+	}
+	return
 }
 
-func newKeyChecker(c *config.VerifyKeyParams) (*keyChecker, error) {
-	return &keyChecker{
-		keyParam:     c.KeyParameter,
-		organization: c.Organization,
-		environment:  c.Environment,
-	}, nil
+func (b keyCheckBuilder) NewListsAspect(env adapter.Env, c adapter.Config) (adapter.ListsAspect, error) {
+	cfg := c.(*config.VerifyKeyParams)
+	var basePath string
+
+	if cfg.VerificationURL == "" {
+		basePath = fmt.Sprintf("https://%s-%s.apigee.net/edgemicro-auth",
+			cfg.Organization, cfg.Environment)
+	} else {
+		basePath = cfg.VerificationURL
+	}
+
+	kc := &keyChecker{
+		env:      env,
+		checkURL: basePath + verifyKeyPath,
+	}
+	env.Logger().Infof("Created Apigee Key Checker to invoke \"%s\"", kc.checkURL)
+
+	return kc, nil
 }
 
 func (l *keyChecker) Close() error {
@@ -75,28 +98,22 @@
 // Test command ./bazel-bin/cmd/client/mixc check -a target.service=f.default.svc.cluster.local --stringmap_attributes request.headers=x-api-key:1tu9pl04Srua2MtsAGtu6ViPxSYSSX2I
 
 func (l *keyChecker) CheckList(symbol string) (bool, error) {
-	fmt.Printf("*** Going to check \"%s\" against \"%s\"\n", symbol, l.organization)
-
-	edge_url := "https://" + l.organization + "-" + l.environment + ".apigee.net/edgemicro-auth/verifyApiKey"
-
-	fmt.Printf("*** edge_url  \"%s\"\n", edge_url)
-
-	return verifyApiKey(symbol, edge_url), nil
-}
-
-func verifyApiKey(apiKey string, uri string) bool {
-	apiKeyBody := APIKeyBody{
-		APIKey: apiKey,
+	apiKeyBody := common.VerifyAPIKeyRequest{
+		Key: symbol,
 	}
-	serializedBody, _ := json.Marshal(&apiKeyBody)
-	req, _ := http.NewRequest("POST", uri, bytes.NewBuffer(serializedBody))
-	req.Header.Add("x-dna-api-key", apiKey)
-	req.Header.Add("content-type", "application/json")
-	client := &http.Client{}
-	resp, _ := client.Do(req)
+	requestBody, _ := json.Marshal(&apiKeyBody)
+
+	resp, err := http.DefaultClient.Post(l.checkURL, "application/json",
+		bytes.NewBuffer(requestBody))
+	if err != nil {
+		l.env.Logger().Errorf("Error contacting verification service: %s", err)
+		return false, err
+	}
+	defer resp.Body.Close()
+
 	if resp.StatusCode != 200 {
-		return false
+		return false, nil
 	} else {
-		return true
+		return true, nil
 	}
 }
diff --git a/adapter/apigeeKeyChecker_test.go b/adapter/apigeeKeyChecker_test.go
index 6f4862d..51d99ce 100644
--- a/adapter/apigeeKeyChecker_test.go
+++ b/adapter/apigeeKeyChecker_test.go
@@ -14,4 +14,124 @@
 limitations under the License.
 */
 
-package apigee
+package adapter
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/apid/istioApigeeAdapter/adapter/config"
+	"github.com/apid/istioApigeeAdapter/mock"
+	"istio.io/mixer/pkg/adapter"
+)
+
+var mockServer *mock.MockServer
+var mockEnv adapter.Env
+
+func TestMain(m *testing.M) {
+	var err error
+	mockEnv = mock.NewMockEnvironment()
+	mockServer, err = mock.StartMockServer(0)
+	if err != nil {
+		panic(err.Error())
+	}
+
+	result := m.Run()
+	mockServer.Stop()
+	os.Exit(result)
+}
+
+func TestMissingOrg(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Environment: "test",
+	}
+	builder := newKeyCheckBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce == nil {
+		t.Fatal("Config should have returned an error")
+	}
+	fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestMissingEnv(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Organization: "test",
+	}
+	builder := newKeyCheckBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce == nil {
+		t.Fatal("Config should have returned an error")
+	}
+	fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestInvalidURL(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Organization:    "foo",
+		Environment:     "test",
+		VerificationURL: ":",
+	}
+	builder := newKeyCheckBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce == nil {
+		t.Fatal("Config should have returned an error")
+	}
+	fmt.Printf("Errors: %s\n", ce.Multi)
+}
+
+func TestValidKey(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Organization:    "foo",
+		Environment:     "test",
+		VerificationURL: "http://" + mockServer.Address(),
+	}
+
+	builder := newKeyCheckBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce != nil {
+		t.Fatalf("Error validating config: %s", ce)
+	}
+
+	aspect, err := builder.NewListsAspect(mockEnv, cfg)
+	if err != nil {
+		t.Fatalf("Error creating aspect: %s", err)
+	}
+	defer aspect.Close()
+
+	result, err := aspect.CheckList(mock.ValidAPIKey1)
+	if err != nil {
+		t.Fatalf("Error on list check: %s", err)
+	}
+	if !result {
+		t.Fatal("List check returned false")
+	}
+}
+
+func TestInvalidKey(t *testing.T) {
+	cfg := &config.VerifyKeyParams{
+		Organization:    "foo",
+		Environment:     "test",
+		VerificationURL: "http://" + mockServer.Address(),
+	}
+
+	builder := newKeyCheckBuilder()
+	ce := builder.ValidateConfig(cfg)
+	if ce != nil {
+		t.Fatalf("Error validating config: %s", ce)
+	}
+
+	aspect, err := builder.NewListsAspect(mockEnv, cfg)
+	if err != nil {
+		t.Fatalf("Error creating aspect: %s", err)
+	}
+	defer aspect.Close()
+
+	result, err := aspect.CheckList("99999")
+	if err != nil {
+		t.Fatalf("Error on list check: %s", err)
+	}
+	if result {
+		t.Fatal("List check returned true and should have failed")
+	}
+}
diff --git a/adapter/apigeeReport.go b/adapter/apigeeReport.go
index d9f9325..2500b57 100644
--- a/adapter/apigeeReport.go
+++ b/adapter/apigeeReport.go
@@ -14,7 +14,7 @@
 limitations under the License.
 */
 
-package apigee
+package adapter
 
 import (
 	"bytes"
diff --git a/adapter/config/config.proto b/adapter/config/config.proto
index 48c7c6c..4d85bc7 100644
--- a/adapter/config/config.proto
+++ b/adapter/config/config.proto
@@ -1,28 +1,32 @@
-// Copyright 2017 Istio 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.
+/*
+Copyright 2017 Google Inc.
+
+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.
+*/
 
 syntax = "proto3";
 
 package config;
 
 message VerifyKeyParams {
-  // The name of the parameter where the API key is stored.
-  string key_parameter = 1;
-
-  string organization = 2;
-
-  string environment = 3;
+  // The name of the Apigee organization -- required
+  string organization = 1;
+  // The name of the Apigee environment -- required
+  string environment = 2;
+  // A URL to use to contact the verification service. Will be
+  // constructed from the organization and environment name if not
+  // specified, and assumes that the service runs at production "apigee.net".
+  string verificationURL = 3;
 }
 
 message ReportParams {
diff --git a/cmd/mockserver/BUILD b/cmd/mockserver/BUILD
new file mode 100644
index 0000000..0b55061
--- /dev/null
+++ b/cmd/mockserver/BUILD
@@ -0,0 +1,13 @@
+package(default_visibility = ["//visibility:public"])
+
+load("@io_bazel_rules_go//go:def.bzl", "go_binary")
+
+go_binary(
+    name = "mockserver",
+    srcs = [
+        "mockserver.go",
+    ],
+    deps = [
+        "//mock:go_default_library",
+    ],
+)
diff --git a/cmd/mockserver/mockserver.go b/cmd/mockserver/mockserver.go
new file mode 100644
index 0000000..c0c13f6
--- /dev/null
+++ b/cmd/mockserver/mockserver.go
@@ -0,0 +1,54 @@
+/*
+Copyright 2017 Google Inc.
+
+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 main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/apid/istioApigeeAdapter/mock"
+)
+
+func main() {
+	var port int
+	var help bool
+
+	flag.IntVar(&port, "p", 0, "Port to listen on")
+	flag.BoolVar(&help, "h", false, "This help message right here")
+	flag.Parse()
+	if !flag.Parsed() || help {
+		flag.PrintDefaults()
+		os.Exit(2)
+	}
+
+	server, err := mock.StartMockServer(port)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error starting server: %s\n", err)
+		os.Exit(3)
+	}
+	fmt.Printf("Listening on %s\n", server.Address())
+
+	sc := make(chan os.Signal)
+	signal.Notify(sc, syscall.SIGTERM, syscall.SIGINT)
+
+	sig := <-sc
+	fmt.Printf("Exiting on signal %s\n", sig)
+	server.Stop()
+}
diff --git a/mock/BUILD b/mock/BUILD
index 4bf127c..4893bb3 100644
--- a/mock/BUILD
+++ b/mock/BUILD
@@ -6,14 +6,16 @@
     name = "go_default_library",
     srcs = [
         "keys.go",
+        "mockenv.go",
         "mockserver.go",
     ],
     deps = [
         "//common:go_default_library",
-        "@com_github_julienschmidt_httprouter//:go_default_library",
+        "//external:mixer_adapter",
         "@com_github_SermoDigital_jose//crypto:go_default_library",
         "@com_github_SermoDigital_jose//jws:go_default_library",
         "@com_github_SermoDigital_jose//jwt:go_default_library",
+        "@com_github_julienschmidt_httprouter//:go_default_library",
     ],
 )
 
@@ -21,6 +23,7 @@
     name = "small_tests",
     size = "small",
     srcs = [
+        "mockenv_test.go",
         "mockserver_test.go",
     ],
     library = ":go_default_library",
diff --git a/mock/mockenv.go b/mock/mockenv.go
new file mode 100644
index 0000000..1c9d176
--- /dev/null
+++ b/mock/mockenv.go
@@ -0,0 +1,73 @@
+/*
+Copyright 2017 Google Inc.
+
+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 mock
+
+import (
+	"fmt"
+
+	"istio.io/mixer/pkg/adapter"
+)
+
+type mockEnvironment struct {
+	log *mockLogger
+}
+
+/*
+NewMockEnvironment returns an implementation of the Mixer adapter's "Env" class so that
+we can test without launching the whole thing.
+*/
+func NewMockEnvironment() adapter.Env {
+	return &mockEnvironment{
+		log: &mockLogger{},
+	}
+}
+
+func (e *mockEnvironment) Logger() adapter.Logger {
+	return e.log
+}
+
+func (e *mockEnvironment) ScheduleWork(fn adapter.WorkFunc) {
+	go func() {
+		fn()
+	}()
+}
+
+func (e *mockEnvironment) ScheduleDaemon(fn adapter.DaemonFunc) {
+	go func() {
+		fn()
+	}()
+}
+
+type mockLogger struct {
+}
+
+func (l *mockLogger) VerbosityLevel(level adapter.VerbosityLevel) bool {
+	return true
+}
+
+func (l *mockLogger) Infof(format string, args ...interface{}) {
+	fmt.Printf("INFO: "+format+"\n", args...)
+}
+
+func (l *mockLogger) Warningf(format string, args ...interface{}) {
+	fmt.Printf("WARN: "+format+"\n", args...)
+}
+
+func (l *mockLogger) Errorf(format string, args ...interface{}) error {
+	fmt.Printf("ERR: "+format+"\n", args...)
+	return nil
+}
diff --git a/mock/mockenv_test.go b/mock/mockenv_test.go
new file mode 100644
index 0000000..a312e1b
--- /dev/null
+++ b/mock/mockenv_test.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2017 Google Inc.
+
+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 mock
+
+import (
+	"testing"
+)
+
+func TestMockWork(t *testing.T) {
+	env := NewMockEnvironment()
+
+	ch := make(chan bool)
+	env.Logger().Infof("Starting mock environment")
+
+	env.ScheduleWork(func() {
+		env.Logger().Infof("Did the work %d times", 1)
+		ch <- true
+	})
+
+	<-ch
+}
+
+func TestMockDaemon(t *testing.T) {
+	env := NewMockEnvironment()
+
+	ch := make(chan bool)
+	env.Logger().Infof("Starting mock environment")
+
+	env.ScheduleDaemon(func() {
+		env.Logger().Infof("Did the work %d times", 1)
+		ch <- true
+	})
+
+	<-ch
+}
diff --git a/testdata/configroot/scopes/global/adapters.yml b/testdata/configroot/scopes/global/adapters.yml
new file mode 100644
index 0000000..f4f2a68
--- /dev/null
+++ b/testdata/configroot/scopes/global/adapters.yml
@@ -0,0 +1,25 @@
+subject: global
+adapters:
+  - name: default
+    kind: quotas
+    impl: memQuota
+    params:
+  - name: default
+    impl: stdioLogger
+    params:
+      logStream: STDERR
+  - name: prometheus
+    kind: metrics
+    impl: prometheus
+    params:
+  - name: default
+    impl: denyChecker
+  - name: apigeeAPIKey
+    kind: lists
+    impl: apigeeKeyChecker
+    params:
+      organization: gregbrail
+      environment: prod
+      # Add verificationURL to use a different service or URL
+      #verificationURL: https://gregbrail-prod.apigee.net/edgemicro-auth
+
diff --git a/testdata/configroot/scopes/global/descriptors.yml b/testdata/configroot/scopes/global/descriptors.yml
new file mode 100644
index 0000000..341513e
--- /dev/null
+++ b/testdata/configroot/scopes/global/descriptors.yml
@@ -0,0 +1,191 @@
+subject: namespace:ns
+revision: "2022"
+manifests:
+  - name: kubernetes
+    revision: "1"
+    attributes:
+      source.ip:
+        valueType: IP_ADDRESS
+      source.labels:
+        valueType: STRING_MAP
+      source.name:
+        valueType: STRING
+      source.namespace:
+        valueType: STRING
+      source.service:
+        valueType: STRING
+      source.serviceAccount:
+        valueType: STRING
+      target.ip:
+        valueType: IP_ADDRESS
+      target.labels:
+        valueType: STRING_MAP
+      target.name:
+        valueType: STRING
+      target.namespace:
+        valueType: STRING
+      target.service:
+        valueType: STRING
+      target.serviceAccount:
+        valueType: STRING
+  - name: istio-proxy
+    revision: "1"
+    attributes:
+      origin.ip:
+        valueType: IP_ADDRESS
+      origin.uid:
+        valueType: STRING
+      origin.user:
+        valueType: STRING
+      request.headers:
+        valueType: STRING_MAP
+      request.id:
+        valueType: STRING
+      request.host:
+        valueType: STRING
+      request.method:
+        valueType: STRING
+      request.path:
+        valueType: STRING
+      request.reason:
+        valueType: STRING
+      request.referer:
+        valueType: STRING
+      request.scheme:
+        valueType: STRING
+      request.size:
+        valueType: INT64
+      request.time:
+        valueType: TIMESTAMP
+      request.useragent:
+        valueType: STRING
+      response.code:
+        valueType: INT64
+      response.duration:
+        valueType: DURATION
+      response.headers:
+        valueType: STRING_MAP
+      response.latency:
+        valueType: DURATION
+      response.size:
+        valueType: INT64
+      response.time:
+        valueType: TIMESTAMP
+      source.uid:
+        valueType: STRING
+      target.uid:
+        valueType: STRING
+      # DEPRECATED, to be removed. Use request.useragent instead.
+      request.user-agent:
+        valueType: STRING
+# Enums as struct fields can be symbolic names.
+# However enums inside maps *cannot* be symbolic names.
+metrics:
+  - name: request_count
+    kind: COUNTER
+    value: INT64
+    description: request count by source, target, service, and code
+    labels:
+      source: 1 # STRING
+      target: 1 # STRING
+      service: 1 # STRING
+      method: 1 # STRING
+      response_code: 2 # INT64
+  - name: request_duration
+    kind: DISTRIBUTION
+    value: DURATION
+    description: request duration by source, target, and service
+    buckets:
+      explicit_buckets:
+        bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+    # Examples of other possible bucket configurations:
+    #      linear_buckets:
+    #         num_finite_buckets: 10
+    #         offset: 0.001
+    #         width: 0.1
+    #      exponential_buckets:
+    #        num_finite_buckets: 15
+    #        scale: 0.001
+    #        growth_factor: 4
+    labels:
+      source: 1 # STRING
+      target: 1 # STRING
+      service: 1 # STRING
+      method: 1 # STRING
+      response_code: 2 # INT64
+  - name: request_size
+    kind: DISTRIBUTION
+    value: INT64
+    description: request size by source, target, and service
+    buckets:
+      exponentialBuckets:
+        numFiniteBuckets: 8
+        scale: 1
+        growthFactor: 10
+    # Examples of other possible bucket configurations:
+    #      explicit_buckets:
+    #         bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+    #      linear_buckets:
+    #         num_finite_buckets: 10
+    #         offset: 0.001
+    #         width: 0.1
+    labels:
+      source: 1 # STRING
+      target: 1 # STRING
+      service: 1 # STRING
+      method: 1 # STRING
+      response_code: 2 # INT64
+  - name: response_size
+    kind: DISTRIBUTION
+    value: INT64
+    description: response size by source, target, and service
+    buckets:
+      exponentialBuckets:
+        numFiniteBuckets: 8
+        scale: 1
+        growthFactor: 10
+    # Examples of other possible bucket configurations:
+    #      explicitBuckets:
+    #         bounds: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
+    #      linearBuckets:
+    #         numFiniteBuckets: 10
+    #         offset: 0.001
+    #         width: 0.1
+    labels:
+      source: 1 # STRING
+      target: 1 # STRING
+      service: 1 # STRING
+      method: 1 # STRING
+      response_code: 2 # INT64
+quotas:
+  - name: RequestCount
+    rate_limit: true
+logs:
+  - name: accesslog.common
+    display_name: Apache Common Log Format
+    payload_format: TEXT
+    log_template: '{{or (.originIp) "-"}} - {{or (.sourceUser) "-"}} [{{or (.timestamp.Format "02/Jan/2006:15:04:05 -0700") "-"}}] "{{or (.method) "-"}} {{or (.url) "-"}} {{or (.protocol) "-"}}" {{or (.responseCode) "-"}} {{or (.responseSize) "-"}}'
+    labels:
+      originIp: 6 # IP_ADDRESS
+      sourceUser: 1 # STRING
+      timestamp: 5 # TIMESTAMP
+      method: 1 # STRING
+      url: 1 # STRING
+      protocol: 1 # STRING
+      responseCode: 2 # INT64
+      responseSize: 2 # INT64
+  - name: accesslog.combined
+    display_name: Apache Combined Log Format
+    payload_format: TEXT
+    log_template: '{{or (.originIp) "-"}} - {{or (.sourceUser) "-"}} [{{or (.timestamp.Format "02/Jan/2006:15:04:05 -0700") "-"}}] "{{or (.method) "-"}} {{or (.url) "-"}} {{or (.protocol) "-"}}" {{or (.responseCode) "-"}} {{or (.responseSize) "-"}} {{or (.referer) "-"}} {{or (.userAgent) "-"}}'
+    labels:
+      originIp: 6 # IP_ADDRESS
+      sourceUser: 1 # STRING
+      timestamp: 5 # TIMESTAMP
+      method: 1 # STRING
+      url: 1 # STRING
+      protocol: 1 # STRING
+      responseCode: 2 # INT64
+      responseSize: 2 # INT64
+      referer: 1 # STRING
+      userAgent: 1 # STRING
diff --git a/testdata/configroot/scopes/global/subjects/global/rules.yml b/testdata/configroot/scopes/global/subjects/global/rules.yml
new file mode 100644
index 0000000..f38981a
--- /dev/null
+++ b/testdata/configroot/scopes/global/subjects/global/rules.yml
@@ -0,0 +1,59 @@
+subject: namespace:ns
+revision: "2022"
+rules:
+- selector: # must be empty for preprocessing adapters
+  aspects:
+  - kind: lists
+    adapter: apigeeAPIKey
+    params:
+      checkExpression: request.headers["apikey"]
+  - kind: quotas
+    params:
+      quotas:
+      - descriptorName: RequestCount
+        maxAmount: 5000
+        expiration: 1s
+  - kind: metrics
+    adapter: prometheus
+    params:
+      metrics:
+      - descriptor_name: request_count
+        # we want to increment this counter by 1 for each unique (source, target, service, method, response_code) tuple
+        value: "1"
+        labels:
+          source: source.labels["app"] | "unknown"
+          target: target.service | "unknown"
+          service: target.labels["app"] | "unknown"
+          method: request.path | "unknown"
+          response_code: response.code | 200
+      - descriptor_name:  request_duration
+        value: response.latency | response.duration | "0ms"
+        labels:
+          source: source.labels["app"] | "unknown"
+          target: target.service | "unknown"
+          service: target.labels["app"] | "unknown"
+          method: request.path | "unknown"
+          response_code: response.code | 200
+  - kind: access-logs
+    params:
+      logName: access_log
+      log:
+        descriptor_name: accesslog.common
+        template_expressions:
+           originIp: origin.ip
+           sourceUser: origin.user
+           timestamp: request.time
+           method: request.method
+           url: request.path
+           protocol: request.scheme
+           responseCode: response.code
+           responseSize: response.size
+        labels:
+           originIp: origin.ip
+           sourceUser: origin.user
+           timestamp: request.time
+           method: request.method
+           url: request.path
+           protocol: request.scheme
+           responseCode: response.code
+           responseSize: response.size