api/schema: initial take on MethodTestCase

The initial thought here is to introduce a MethodTestCase type that
encapsulates the behavior of testing a single method in a particular given
service.

To do so, a httptest.Server is created and the schema is turned into a request
which is fired at that server. Thet MethodTestCase, of course, knows how to
respond to different requests, and the behavior of those responses is tested.

What I dislike is that we have to write three things which are mostly the same
to test any endpoint in any case on the API:
  1) a request type (Go struct)
  2) an expected response type (Go type)
  3) the actual response (a mutltline Go string, which is really just JSON)

This seems redundant, so I may explore other options for implementing this sort
of thing in the future.
This commit is contained in:
Taylor Blau 2016-05-18 15:44:34 -06:00
parent 9234838551
commit c282983235
5 changed files with 240 additions and 1 deletions

@ -23,6 +23,8 @@ const (
// fmt.Println(apiResponse.Lock)
// ```
type Client struct {
Locks LockService
// base is root URL that all requests will be made against. It is
// initialized when the client is constructed, and remains immutable
// throughout the duration of the *Client.

@ -89,7 +89,7 @@ func (l *HttpLifecycle) Execute(req *http.Request, into interface{}) (Response,
if into != nil {
decoder := json.NewDecoder(resp.Body)
if err = decoder.Decode(&into); err != nil {
if err = decoder.Decode(into); err != nil {
return nil, err
}
}

124
api/lock_api.go Normal file

@ -0,0 +1,124 @@
package api
import (
"fmt"
"net/http"
"time"
)
type LockService struct {
}
func (s *LockService) Lock(req *LockRequest) (*RequestSchema, *LockResponse) {
var resp LockResponse
return &RequestSchema{
Method: http.MethodPost,
Path: "/locks",
Body: req,
Into: &resp,
}, &resp
}
func (s *LockService) Unlock(l *Lock) (*RequestSchema, UnlockResult) {
var resp UnlockResult
return &RequestSchema{
Method: http.MethodPost,
Path: fmt.Sprintf("/locks/%s/unlock", l.Id),
Into: resp,
}, resp
}
// Lock represents a single lock that against a particular path.
//
// Locks returned from the API may or may not be currently active, according to
// the Expired flag.
type Lock struct {
// Id is the unique identifier corresponding to this particular Lock. It
// must be consistent with the local copy, and the server's copy.
Id string `json:"id"`
// Path is an absolute path to the file that is locked as a part of this
// lock.
Path string `json:"path"`
// Committer is the author who initiated this lock.
Committer Committer `json:"committer"`
// CommitSHA is the commit that this Lock was created against. It is
// strictly equal to the SHA of the minimum commit negotiated in order
// to create this lock.
CommitSHA string `json:"commit_sha"`
// LockedAt is a required parameter that represents the instant in time
// that this lock was created. For most server implementations, this
// should be set to the instant at which the lock was initially
// received.
LockedAt time.Time `json:"locked_at"`
// ExpiresAt is an optional parameter that represents the instant in
// time that the lock stopped being active. If the lock is still active,
// the server can either a) not send this field, or b) send the
// zero-value of time.Time.
UnlockedAt time.Time `json:"unlocked_at,omitempty"`
}
// Active returns whether or not the given lock is still active against the file
// that it is protecting.
func (l *Lock) Active() bool {
return l.UnlockedAt.IsZero()
}
type Committer struct {
// Name is the name of the individual who would like to obtain the
// lock, for instance: "Rick Olson".
Name string `json:"name"`
// Email is the email assopsicated with the individual who would
// like to obtain the lock, for instance: "rick@github.com".
Email string `json:"email"`
}
// LockRequest encapsulates the payload sent across the API when a client would
// like to obtain a lock against a particular path on a given remote.
type LockRequest struct {
// Path is the path that the client would like to obtain a lock against.
Path string `json:"path"`
// LatestRemoteCommit is the SHA of the last known commit from the
// remote that we are trying to create the lock against, as found in
// `.git/refs/origin/<name>`.
LatestRemoteCommit string `json:"latest_remote_commit"`
// Committer is the individual that wishes to obtain the lock.
Committer Committer `json:"committer"`
}
// LockResponse encapsulates the information sent over the API in response to
// a `LockRequest`.
type LockResponse struct {
// Lock is the Lock that was optionally created in response to the
// payload that was sent (see above). If the lock already exists, then
// the existing lock is sent in this field instead, and the author of
// that lock remains the same, meaning that the client failed to obtain
// that lock. An HTTP status of "409 - Conflict" is used here.
//
// If the lock was unable to be created, this field will hold the
// zero-value of Lock and the Err field will provide a more detailed set
// of information.
//
// If an error was experienced in creating this lock, then the
// zero-value of Lock should be sent here instead.
Lock Lock `json:"lock"`
// CommitNeeded holds the minimum commit SHA that client must have to
// obtain the lock.
CommitNeeded string `json:"commit_needed,omitempty"`
// Err is the optional error that was encountered while trying to create
// the above lock.
Err error `json:"error,omitempty"`
}
// UnlockResult is the result sent back from the API when asked to remove a
// lock.
type UnlockResult struct {
// Lock is the lock corresponding to the asked-about lock in the
// `UnlockPayload` (see above). If no matching lock was found, this
// field will take the zero-value of Lock, and Err will be non-nil.
Lock Lock `json:"lock"`
// Err is an optional field which holds any error that was experienced
// while removing the lock.
Err error `json:"error,omitempty"`
}

91
api/lock_api_test.go Normal file

@ -0,0 +1,91 @@
package api_test
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/github/git-lfs/api"
"github.com/stretchr/testify/assert"
)
var LockService api.LockService
func TestSuccessfullyObtainingALock(t *testing.T) {
schema, resp := LockService.Lock(&api.LockRequest{
Path: "/path/to/file",
LatestRemoteCommit: "deadbeef",
Committer: api.Committer{
Name: "Jane Doe",
Email: "jane@example.com",
},
})
tc := &MethodTestCase{
Schema: schema,
Response: resp,
ExpectedPath: "/locks",
ExpectedMethod: http.MethodPost,
ExpectedResponse: &api.LockResponse{
Lock: api.Lock{
Id: "some-lock-id",
Path: "/path/to/file",
Committer: api.Committer{
Name: "Jane Doe",
Email: "jane@example.com",
},
CommitSHA: "deadbeef",
LockedAt: time.Date(2016, time.May, 18, 0, 0, 0, 0, time.UTC),
},
},
Output: `
{
"lock": {
"id": "some-lock-id",
"path": "/path/to/file",
"committer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"commit_sha": "deadbeef",
"locked_at": "2016-05-18T00:00:00Z"
}
}`,
}
tc.Assert(t)
}
type MethodTestCase struct {
Schema *api.RequestSchema
Response interface{}
ExpectedPath string
ExpectedMethod string
ExpectedResponse interface{}
Output string
}
func (c *MethodTestCase) Assert(t *testing.T) {
var called bool
server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, req *http.Request) {
called = true
w.Write([]byte(c.Output))
assert.Equal(t, c.ExpectedPath, req.URL.String())
assert.Equal(t, c.ExpectedMethod, req.Method)
},
))
client, _ := api.NewClient(server.URL)
fmt.Println(client.Do(c.Schema))
assert.Equal(t, true, called, "lfs/api: expected method %s to be called", c.ExpectedPath)
assert.Equal(t, c.ExpectedResponse, c.Response)
}

22
commands/command_lock.go Normal file

@ -0,0 +1,22 @@
package commands
import (
"fmt"
"github.com/github/git-lfs/vendor/_nuts/github.com/spf13/cobra"
)
var (
lockCmd = &cobra.Command{
Use: "lock",
Run: lockCommand,
}
)
func lockCommand(cmd *cobra.Command, args []string) {
fmt.Println("I was run!")
}
func init() {
RootCmd.AddCommand(lockCmd)
}