[secrets] Create go library for starting up a 'secrets' server.
Bug: IN-558
Tested: unit tests added
Change-Id: Iea5b079f3914a510aa5f9f5104e3029c90c9014e
diff --git a/secrets/server.go b/secrets/server.go
new file mode 100644
index 0000000..696f9e3
--- /dev/null
+++ b/secrets/server.go
@@ -0,0 +1,78 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package secrets
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "go.chromium.org/luci/lucictx"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+// A mapping of test name to associated secret.
+type testSecrets map[string]string
+
+// Serves the secret associated to a test, where the request's URL is of the
+// form "/<test name>".
+func (secrets testSecrets) serveSecret(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain")
+ testName := strings.TrimPrefix(r.URL.Path, "/")
+ secret, ok := secrets[testName]
+ if !ok {
+ log.Printf("There is no secret to serve for \"%s\"\n", testName)
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+ log.Printf("Serving secret for \"%s\"\n", testName)
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(secret))
+}
+
+// Parses out tests secrets serialized in the LUCI_CONTEXT JSON under the
+// "secret_bytes" key.
+func getSecrets(ctx context.Context) (*testSecrets, error) {
+ swarming := lucictx.GetSwarming(ctx)
+ if swarming == nil {
+ return nil, fmt.Errorf("GetSwarming() returned a null value.")
+ }
+ secrets := new(testSecrets)
+ err := json.Unmarshal(swarming.SecretBytes, secrets)
+ return secrets, err
+}
+
+// Starts a server to serve test secrets at localhost:<|port|>.
+func StartSecretsServer(ctx context.Context, port int) {
+ secrets, err := getSecrets(ctx)
+ if err != nil {
+ log.Printf("Unable to find secrets: %v\n", err)
+ return
+ }
+
+ log.Printf("Setting up secrets server at localhost:%d\n", port)
+ s := &http.Server{
+ Addr: ":" + strconv.Itoa(port),
+ Handler: http.HandlerFunc(secrets.serveSecret),
+ }
+
+ go func() {
+ if err := s.ListenAndServe(); err != nil {
+ log.Print(err)
+ }
+ }()
+
+ go func() {
+ select {
+ case <-ctx.Done():
+ log.Printf("Shutting down secrets server at localhost:%d\n", port)
+ if err := s.Shutdown(context.Background()); err != nil {
+ log.Print(err)
+ }
+ default:
+ }
+ }()
+}
diff --git a/secrets/server_test.go b/secrets/server_test.go
new file mode 100644
index 0000000..786b0a0
--- /dev/null
+++ b/secrets/server_test.go
@@ -0,0 +1,74 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package secrets
+
+import (
+ "context"
+ "go.chromium.org/luci/lucictx"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+)
+
+func TestGettingSecrets(t *testing.T) {
+ secretBytes := []byte(`{"testNameA":"SECRETA","testNameB":"SECRETB"}`)
+ swarming := lucictx.Swarming{
+ SecretBytes: secretBytes,
+ }
+ ctx := lucictx.SetSwarming(context.Background(), &swarming)
+
+ expectedSecrets := testSecrets{
+ "testNameA": "SECRETA",
+ "testNameB": "SECRETB",
+ }
+ actualSecrets, err := getSecrets(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(actualSecrets, &expectedSecrets) {
+ t.Errorf("Returned secrets \"%v\" do not match the expected: \"%v\"\n",
+ *actualSecrets, expectedSecrets)
+ }
+}
+
+func TestServingSecrets(t *testing.T) {
+ // Returns an GET request for the secret associated to |testName|.
+ secretRequest := func(testName string) *http.Request {
+ request, err := http.NewRequest(http.MethodGet, "/"+testName, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return request
+ }
+
+ secrets := testSecrets{
+ "foo_unittests": "FOO-SECRET",
+ "bar_e2e_tests": "BAR-SECRET",
+ }
+ handler := http.HandlerFunc(secrets.serveSecret)
+
+ // Checks that the expected content and code were returned in a mock HTTP response.
+ expectValidResponse := func(t *testing.T, testName string, expectedCode int,
+ expectedContent string) {
+ recorder := httptest.NewRecorder()
+ handler.ServeHTTP(recorder, secretRequest(testName))
+ if actualCode := recorder.Code; actualCode != expectedCode {
+ t.Errorf("serveSecret() response code: %v\n; %v was expected for test \"%v\"\n",
+ actualCode, expectedCode, testName)
+ }
+ if actualContent := recorder.Body.String(); actualContent != expectedContent {
+ t.Errorf("serveSecret() failed to returned \"%v\" instead of \"%v\" for test \"%v\"\n",
+ actualContent, expectedContent, testName)
+ }
+ }
+
+ t.Run("Succeeds when associated secret exists", func(t *testing.T) {
+ expectValidResponse(t, "foo_unittests", http.StatusOK, "FOO-SECRET")
+ })
+
+ t.Run("Fails when associated secret does not exist", func(t *testing.T) {
+ expectValidResponse(t, "non_existant_unittests", http.StatusNotFound, "")
+ })
+}