Initial docs & scaffold for server API tests

This commit is contained in:
Steve Streeting 2015-11-24 11:55:33 +00:00
parent 3c86ea1ea3
commit eca6d99d99
3 changed files with 201 additions and 0 deletions

@ -0,0 +1 @@
git-lfs-test-server-api*

@ -0,0 +1,45 @@
# Git LFS Server API compliance test utility
This package exists to provide automated testing of server API implementations,
to ensure that they conform to the behaviour expected by the client. You can
run this utility against any server that implements the Git LFS API.
## Automatic or data-driven testing
This utility is primarily intended to test the API implementation, but in order
to correctly test the responses, the tests have to know what objects exist on
the server already and which don't.
In 'automatic' mode, the tests require that both the API and the content server
it links to via upload and download links are both available & free to use.
The content server must be empty at the start of the tests, and the tests will
upload some data as part of the tests. Therefore obviously this cannot be a
production system.
Alternatively, in 'data-driven' mode, the tests must be provided with a list of
object IDs that already exist on the server (minimum 10), and a list of other
object IDs that are known to not exist. The test will use these IDs to
construct its data sets, will only call the API (not the content server), and
thus will not update any data - meaning you can in theory run this against a
production system.
## Calling the test tool
```
git-lfs-test-server-api [--url=<apiurl> | --clone=<cloneurl>]
[<oid-exists-file> <oid-missing-file>]
```
|Argument|Purpose|
|------|-------|
|`--url=<apiurl>`|URL of the server API to call. This must point directly at the API root and not the clone URL, and must be HTTP[S]. You must supply either this argument or the `--clone` argument|
|`--clone=<cloneurl>`|The clone URL from which to derive the API URL. If it is HTTP[S], the test will try to find the API at `<cloneurl>/info/lfs`; if it is an SSH URL, then the test will call git-lfs-authenticate on the server to derive the API (with auth token if needed) just like the git-lfs client does. You must supply either this argument or the `--url` argument|
|`<oid-exists-file> <oid-missing-file>`|Optional input files for data-driven mode (both must be supplied if this is used); each must be a file with one oid per line. The first must be a list of oids that exist on the server, the second must bea list of oids known not to exist. If supplied, the tests will not call the content server or modify any data. If omitted, the test will generate its own list of oids and will modify the server (and expects that the server is empty of oids at the start)|
## Authentication
Authentication will behave just like the git-lfs client, so for HTTP[S] URLs the
git credential helper system will be used to obtain logins, and for SSH URLs,
keys can be used to automate login. Otherwise you will receive prompts on the
command line.

@ -0,0 +1,155 @@
package main
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"os"
"strings"
"github.com/github/git-lfs/lfs"
"github.com/github/git-lfs/vendor/_nuts/github.com/spf13/cobra"
)
type ServerTest struct {
Name string
F func(endp lfs.Endpoint, oidsExist, oidsMissing []string) error
}
var (
RootCmd = &cobra.Command{
Use: "git-lfs-test-server-api [--url=<apiurl> | --clone=<cloneurl>] [<oid-exists-file> <oid-missing-file>]",
Short: "Test a Git LFS API server for compliance",
Run: testServerApi,
}
apiUrl string
cloneUrl string
tests []ServerTest
)
func main() {
RootCmd.Execute()
}
func testServerApi(cmd *cobra.Command, args []string) {
if (len(apiUrl) == 0 && len(cloneUrl) == 0) ||
(len(apiUrl) != 0 && len(cloneUrl) != 0) {
exit("Must supply either --url or --clone (and not both)")
}
if len(args) != 0 && len(args) != 2 {
exit("Must supply either no file arguments or both the exists AND missing file")
}
var endp lfs.Endpoint
if len(cloneUrl) > 0 {
endp = lfs.NewEndpointFromCloneURL(cloneUrl)
} else {
endp = lfs.NewEndpoint(apiUrl)
}
var oidsExist, oidsMissing []string
if len(args) >= 2 {
fmt.Printf("Reading test data from files (no server content changes)\n")
oidsExist = readTestOids(args[0])
oidsMissing = readTestOids(args[1])
} else {
fmt.Printf("Creating test data (will modify server contents)\n")
oidsExist, oidsMissing = constructTestOids()
// Run a 'test' which is really just a setup task, but because it has to
// use the same APIs it's a test in its own right too
err := runTest(ServerTest{"Set up test data", setupTestData}, endp, oidsExist, oidsMissing)
if err != nil {
exit("Failed to set up test data, aborting")
}
}
runTests(endp, oidsExist, oidsMissing)
}
func readTestOids(filename string) []string {
f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
if err != nil {
exit("Error opening file %s", filename)
}
defer f.Close()
var ret []string
rdr := bufio.NewReader(f)
line, err := rdr.ReadString('\n')
for err == nil {
ret = append(ret, strings.TrimSpace(line))
line, err = rdr.ReadString('\n')
}
return ret
}
func constructTestOids() (oidsExist, oidsMissing []string) {
const oidCount = 50
oidsExist = make([]string, 0, oidCount)
oidsMissing = make([]string, 0, oidCount)
// Generate SHAs, not random so repeatable
rand.Seed(int64(oidCount))
runningSha := sha256.New()
for i := 0; i < oidCount; i++ {
runningSha.Write([]byte{byte(rand.Intn(256))})
oid := hex.EncodeToString(runningSha.Sum(nil))
oidsExist = append(oidsExist, oid)
runningSha.Write([]byte{byte(rand.Intn(256))})
oid = hex.EncodeToString(runningSha.Sum(nil))
oidsMissing = append(oidsMissing, oid)
}
return
}
func runTests(endp lfs.Endpoint, oidsExist, oidsMissing []string) {
fmt.Printf("Running %d tests...\n", len(tests))
for _, t := range tests {
runTest(t, endp, oidsExist, oidsMissing)
}
}
func runTest(t ServerTest, endp lfs.Endpoint, oidsExist, oidsMissing []string) error {
const linelen = 70
line := t.Name
if len(line) > linelen {
line = line[:linelen]
} else if len(line) < linelen {
line = fmt.Sprintf("%s%s", line, strings.Repeat(" ", linelen-len(line)))
}
fmt.Printf("%s...\r", line)
err := t.F(endp, oidsExist, oidsMissing)
if err != nil {
fmt.Printf("%s FAILED\n", line)
fmt.Println(err.Error())
} else {
fmt.Printf("%s OK\n", line)
}
return err
}
func setupTestData(endp lfs.Endpoint, oidsExist, oidsMissing []string) error {
// TODO
return nil
}
// Exit prints a formatted message and exits.
func exit(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(2)
}
func init() {
RootCmd.Flags().StringVarP(&apiUrl, "url", "u", "", "URL of the API (must supply this or --clone)")
RootCmd.Flags().StringVarP(&cloneUrl, "clone", "c", "", "Clone URL from which to find API (must supply this or --url)")
}