package integration
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"testing"
knet "k8s.io/kubernetes/pkg/util/net"
configapi "github.com/openshift/origin/pkg/cmd/server/api"
testutil "github.com/openshift/origin/test/util"
testserver "github.com/openshift/origin/test/util/server"
)
func TestWebConsoleExtensions(t *testing.T) {
// Create a temporary directory.
tmpDir, err := ioutil.TempDir("", "extensions")
if err != nil {
t.Fatalf("Could not create tmp dir for extensions: %v", err)
return
}
defer os.RemoveAll(tmpDir)
// Create extension files.
var testData = map[string]string{
"script1.js": script1,
"script2.js": script2,
"stylesheet1.css": stylesheet1,
"stylesheet2.css": stylesheet2,
"extension1/index.html": index,
"extension2/index.html": index,
"extension1/files/shakespeare.txt": plaintext,
}
for path, content := range testData {
if err := os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, path)), 0755); err != nil {
t.Fatalf("Failed creating directory for %s: %v", path, err)
return
}
if err := ioutil.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0755); err != nil {
t.Fatalf("Failed creating file %s: %v", path, err)
return
}
}
// Build master config.
testutil.RequireEtcd(t)
defer testutil.DumpEtcdOnFailure(t)
masterOptions, err := testserver.DefaultMasterOptions()
if err != nil {
t.Fatalf("Failed creating master configuration: %v", err)
return
}
masterOptions.AssetConfig.ExtensionScripts = []string{
filepath.Join(tmpDir, "script1.js"),
filepath.Join(tmpDir, "script2.js"),
}
masterOptions.AssetConfig.ExtensionStylesheets = []string{
filepath.Join(tmpDir, "stylesheet1.css"),
filepath.Join(tmpDir, "stylesheet2.css"),
}
masterOptions.AssetConfig.Extensions = []configapi.AssetExtensionsConfig{
{
Name: "extension1",
SourceDirectory: filepath.Join(tmpDir, "extension1"),
HTML5Mode: true,
},
{
Name: "extension2",
SourceDirectory: filepath.Join(tmpDir, "extension2"),
HTML5Mode: false,
},
}
// Start server.
_, err = testserver.StartConfiguredMaster(masterOptions)
if err != nil {
t.Fatalf("Unexpected error starting server: %v", err)
return
}
// Inject the base into index.html to test HTML5Mode
publicURL, err := url.Parse(masterOptions.AssetConfig.PublicURL)
if err != nil {
t.Fatalf("Unexpected error parsing PublicURL %q: %v", masterOptions.AssetConfig.PublicURL, err)
return
}
baseInjected := injectBase(index, "extension1", publicURL)
// TODO: Add tests for caching.
testcases := map[string]struct {
URL string
Status int
Type string
Content []byte
RedirectLocation string
}{
"extension scripts": {
URL: "scripts/extensions.js",
Status: http.StatusOK,
Type: "text/javascript",
Content: []byte(script1 + ";\n" + script2 + ";\n"),
},
"extension css": {
URL: "styles/extensions.css",
Status: http.StatusOK,
Type: "text/css",
Content: []byte(stylesheet1 + "\n" + stylesheet2 + "\n"),
},
"extension index.html (html5Mode on)": {
URL: "extensions/extension1/",
Status: http.StatusOK,
Type: "text/html",
Content: baseInjected,
},
"extension index.html (html5Mode off)": {
URL: "extensions/extension2/",
Status: http.StatusOK,
Type: "text/html",
Content: []byte(index),
},
"extension no trailing slash (html5Mode on)": {
URL: "extensions/extension1",
Status: http.StatusMovedPermanently,
RedirectLocation: "extensions/extension1/",
},
"extension missing file (html5Mode on)": {
URL: "extensions/extension1/does-not-exist/",
Status: http.StatusOK,
Type: "text/html",
Content: baseInjected,
},
"extension missing file (html5Mode on, no trailing slash)": {
URL: "extensions/extension1/does-not-exist",
Status: http.StatusOK,
Type: "text/html",
Content: baseInjected,
},
"extension missing file (html5Mode off)": {
URL: "extensions/extension2/does-not-exist/",
Status: http.StatusNotFound,
},
"extension missing file (html5Mode off, no trailing slash)": {
URL: "extensions/extension2/does-not-exist",
Status: http.StatusNotFound,
},
"extension file in subdir": {
URL: "extensions/extension1/files/shakespeare.txt",
Status: http.StatusOK,
Type: "text/plain",
Content: []byte(plaintext),
},
"extension file from other extension": {
URL: "extensions/extension2/files/shakespeare.txt",
Status: http.StatusNotFound,
},
}
transport := knet.SetTransportDefaults(&http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
})
for k, tc := range testcases {
testURL := masterOptions.AssetConfig.PublicURL + tc.URL
req, err := http.NewRequest("GET", testURL, nil)
if err != nil {
t.Errorf("%s: Unexpected error creating request for %q: %v", k, testURL, err)
continue
}
resp, err := transport.RoundTrip(req)
if err != nil {
t.Errorf("%s: Unexpected error while accessing %q: %v", k, testURL, err)
continue
}
if resp.StatusCode != tc.Status {
t.Errorf("%s: Expected status %d for %q, got %d", k, tc.Status, testURL, resp.StatusCode)
continue
}
if resp.StatusCode == http.StatusOK {
actualType := strings.Split(resp.Header.Get("Content-Type"), ";")[0]
if actualType != tc.Type {
t.Errorf("%s: Expected type %q for %q, got %s", k, tc.Type, testURL, actualType)
continue
}
defer resp.Body.Close()
actualContent, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Errorf("%s: Unexpected error while reading body of %q: %v", k, testURL, err)
continue
}
if !bytes.Equal(actualContent, []byte(tc.Content)) {
t.Errorf("%s: Response body for %q did not match expected, actual:\n%s\nexpected:\n%s", k, testURL, actualContent, tc.Content)
continue
}
}
if len(tc.RedirectLocation) > 0 {
actualLocation := resp.Header.Get("Location")
expectedLocation := publicURL.Path + tc.RedirectLocation
if actualLocation != expectedLocation {
t.Errorf("%s: Expected response header Location %q for %q, got %q", k, expectedLocation, testURL, actualLocation)
continue
}
}
}
}
func injectBase(content, extensionName string, publicURL *url.URL) []byte {
base := path.Join(publicURL.Path, "extensions", extensionName) + "/"
return bytes.Replace([]byte(content), []byte(`<base href="/">`), []byte(fmt.Sprintf(`<base href="%s">`, base)), 1)
}
const (
script1 = `$(document).ready(function(){$("body").hide().fadeIn(1000);})`
script2 = `$(document).ready(function() {
$('#openshift-logo img').attr('src', 'http://example.com/images/my-logo.png');
})`
stylesheet1 = `html {
font-family: Gill Sans Extrabold, sans-serif;
font-size: 14px;
}
`
stylesheet2 = `.navbar-header {
background-color: red;
border-bottom: 1px solid white;
}
.btn-primary {
background-color: green;
background-image: none;
}
`
index = `<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<base href="/">
</head>
<body>
<h1>Hello</h1>
<p>Hello, OpenShift!</p>
</body>
</html>
`
plaintext = `Conscience does make cowards of us all, and thus the native hue of resolution is sicklied o'er with the pale cast of thought.
`
)