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:
parent
9234838551
commit
c282983235
@ -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
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
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
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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user