blob: 8e443e22c1a0c6b569e515422d9f9c9b24fab925 [file] [log] [blame]
// Copyright 2015 The Vanadium 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 test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"regexp"
"time"
"v.io/jiri"
"v.io/jiri/collect"
"v.io/jiri/retry"
"v.io/x/devtools/internal/test"
"v.io/x/devtools/internal/xunit"
)
// generateXUnitTestSuite generates an xUnit test suite that
// encapsulates the given input.
func generateXUnitTestSuite(jirix *jiri.X, failure *xunit.Failure, pkg string, duration time.Duration) *xunit.TestSuite {
// Generate an xUnit test suite describing the result.
s := xunit.TestSuite{Name: pkg}
c := xunit.TestCase{
Classname: pkg,
Name: "Test",
Time: fmt.Sprintf("%.2f", duration.Seconds()),
}
if failure != nil {
fmt.Fprintf(jirix.Stdout(), "%s ... failed\n%v\n", pkg, failure.Data)
c.Failures = append(c.Failures, *failure)
s.Failures++
} else {
fmt.Fprintf(jirix.Stdout(), "%s ... ok\n", pkg)
}
s.Tests++
s.Cases = append(s.Cases, c)
return &s
}
// testSingleProdService test the given production service.
func testSingleProdService(jirix *jiri.X, principalDir string, service prodService) *xunit.TestSuite {
bin := filepath.Join(jirix.Root, "release", "go", "bin", "vrpc")
var out bytes.Buffer
start := time.Now()
args := []string{}
if principalDir != "" {
args = append(args, "--v23.credentials", principalDir)
}
args = append(args, "signature", "-s", "--show-reserved")
if principalDir == "" {
args = append(args, "--insecure")
}
args = append(args, service.objectName)
if err := jirix.NewSeq().Capture(&out, &out).Verbose(true).Timeout(test.DefaultTimeout).
Last(bin, args...); err != nil {
fmt.Fprintf(jirix.Stderr(), "Failed running %q: %v. Output:\n%v\n", append([]string{bin}, args...), err, out.String())
return generateXUnitTestSuite(jirix, &xunit.Failure{Message: "vrpc", Data: out.String()}, service.name, time.Now().Sub(start))
}
if !service.regexp.Match(out.Bytes()) {
fmt.Fprintf(jirix.Stderr(), "couldn't match regexp %q in output:\n%v\n", service.regexp, out.String())
return generateXUnitTestSuite(jirix, &xunit.Failure{Message: "vrpc", Data: "mismatching signature"}, service.name, time.Now().Sub(start))
}
return generateXUnitTestSuite(jirix, nil, service.name, time.Now().Sub(start))
}
type prodService struct {
name string // Name to use for the test description
objectName string // Object name of the service to connect to
regexp *regexp.Regexp // Regexp that should match the signature output
}
// vanadiumProdServicesTest runs a test of vanadium production services.
func vanadiumProdServicesTest(jirix *jiri.X, testName string, opts ...Opt) (_ *test.Result, e error) {
// Initialize the test.
// Need the new-stype base profile since many web tests will build
// go apps that need it.
cleanup, err := initTest(jirix, testName, []string{"v23:base"})
if err != nil {
return nil, newInternalError(err, "Init")
}
defer collect.Error(func() error { return cleanup() }, &e)
// Install the vrpc tool.
tmpdir, err := jirix.NewSeq().Run("jiri", "go", "install", "v.io/x/ref/cmd/vrpc").
Run("jiri", "go", "install", "v.io/x/ref/cmd/principal").
TempDir("", "prod-services-test")
if err != nil {
return nil, newInternalError(err, "Installing vrpc and creating testdir")
}
defer collect.Error(func() error { return jirix.NewSeq().RemoveAll(tmpdir).Done() }, &e)
blessingRoot, namespaceRoot := getServiceOpts(opts)
allPassed, suites := true, []xunit.TestSuite{}
// Fetch the "root" blessing that all services are blessed by.
suite, pubkey, blessingNames := testIdentityProviderHTTP(jirix, blessingRoot)
suites = append(suites, *suite)
if suite.Failures == 0 {
// Setup a principal that will be used by testAllProdServices and will
// recognize the blessings of the prod services.
principalDir, err := setupPrincipal(jirix, tmpdir, pubkey, blessingNames)
if err != nil {
return nil, err
}
for _, suite := range testAllProdServices(jirix, principalDir, namespaceRoot) {
allPassed = allPassed && (suite.Failures == 0)
suites = append(suites, *suite)
}
}
// Create the xUnit report.
if err := xunit.CreateReport(jirix, testName, suites); err != nil {
return nil, err
}
for _, suite := range suites {
if suite.Failures > 0 {
// At least one test failed:
return &test.Result{Status: test.Failed}, nil
}
}
return &test.Result{Status: test.Passed}, nil
}
func testAllProdServices(jirix *jiri.X, principalDir, namespaceRoot string) []*xunit.TestSuite {
services := []prodService{
prodService{
name: "mounttable",
objectName: namespaceRoot,
regexp: regexp.MustCompile(`MountTable[[:space:]]+interface`),
},
prodService{
name: "macaroon service",
objectName: namespaceRoot + "/identity/dev.v.io:u/macaroon",
regexp: regexp.MustCompile(`MacaroonBlesser[[:space:]]+interface`),
},
prodService{
name: "google identity service",
objectName: namespaceRoot + "/identity/dev.v.io:u/google",
regexp: regexp.MustCompile(`OAuthBlesser[[:space:]]+interface`),
},
prodService{
objectName: namespaceRoot + "/identity/dev.v.io:u/discharger",
name: "binary discharger",
regexp: regexp.MustCompile(`Discharger[[:space:]]+interface`),
},
prodService{
objectName: namespaceRoot + "/proxy-mon/__debug",
name: "proxy service",
// We just check that the returned signature has the __Reserved interface since
// proxy-mon doesn't implement any other services.
regexp: regexp.MustCompile(`__Reserved[[:space:]]+interface`),
},
}
var suites []*xunit.TestSuite
for _, service := range services {
suites = append(suites, testSingleProdService(jirix, principalDir, service))
}
return suites
}
// testIdentityProviderHTTP tests that the identity provider's HTTP server is
// up and running and also fetches the set of blessing names that the provider
// claims to be authoritative on and the public key (encoded) used by that
// identity provider to sign certificates for blessings.
//
// PARANOIA ALERT:
// This function is subject to man-in-the-middle attacks because it does not
// verify the TLS certificates presented by the server. This does open the
// door for an attack where a parallel universe of services could be setup
// and fool this production services test into thinking all services are
// up and running when they may not be.
//
// The attacker in this case will have to be able to mess with the routing
// tables on the machine running this test, or the network routes of routers
// used by the machine, or mess up DNS entries.
func testIdentityProviderHTTP(jirix *jiri.X, blessingRoot string) (suite *xunit.TestSuite, publickey string, blessingNames []string) {
url := fmt.Sprintf("https://%s/auth/blessing-root", blessingRoot)
var response struct {
Names []string `json:"names"`
PublicKey string `json:"publicKey"`
}
var resp *http.Response
var err error
var start time.Time
fn := func() error {
start = time.Now()
resp, err = http.Get(url)
return err
}
if err = retry.Function(jirix.Context, fn); err == nil {
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&response)
}
var failure *xunit.Failure
if err != nil {
failure = &xunit.Failure{Message: "identityd HTTP", Data: err.Error()}
}
return generateXUnitTestSuite(jirix, failure, url, time.Now().Sub(start)), response.PublicKey, response.Names
}
func setupPrincipal(jirix *jiri.X, tmpdir, pubkey string, blessingNames []string) (string, error) {
s := jirix.NewSeq()
dir := filepath.Join(tmpdir, "credentials")
bin := filepath.Join(jirix.Root, "release", "go", "bin", "principal")
if err := s.Timeout(test.DefaultTimeout).Last(bin, "create", "-with-passphrase=false", dir, "prod-services-tester"); err != nil {
fmt.Fprintf(jirix.Stderr(), "principal create failed: %v\n", err)
return "", err
}
for _, name := range blessingNames {
if err := s.Timeout(test.DefaultTimeout).Last(bin, "--v23.credentials", dir, "recognize", name, pubkey); err != nil {
fmt.Fprintf(jirix.Stderr(), "principal recognize %v %v failed: %v\n", name, pubkey, err)
return "", err
}
}
return dir, nil
}
// getServiceOpts extracts blessing root and namespace root from the
// given Opts.
func getServiceOpts(opts []Opt) (string, string) {
blessingRoot := "dev.v.io"
namespaceRoot := "/ns.dev.v.io:8101"
for _, opt := range opts {
switch v := opt.(type) {
case BlessingsRootOpt:
blessingRoot = string(v)
case NamespaceRootOpt:
namespaceRoot = string(v)
}
}
return blessingRoot, namespaceRoot
}