Add ACME CA capabilities
This commit is contained in:
parent
68ab03dc1b
commit
e3826dd1c3
54 changed files with 15687 additions and 184 deletions
790
acme/api/account_test.go
Normal file
790
acme/api/account_test.go
Normal file
|
@ -0,0 +1,790 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDisableRenewal = false
|
||||
globalProvisionerClaims = provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute},
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DisableRenewal: &defaultDisableRenewal,
|
||||
}
|
||||
)
|
||||
|
||||
func newProv() provisioner.Interface {
|
||||
// Initialize provisioners
|
||||
p := &provisioner.ACME{
|
||||
Type: "ACME",
|
||||
Name: "test@acme-provisioner.com",
|
||||
}
|
||||
if err := p.Init(provisioner.Config{Claims: globalProvisionerClaims}); err != nil {
|
||||
fmt.Printf("%v", err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func TestNewAccountRequestValidate(t *testing.T) {
|
||||
type test struct {
|
||||
nar *NewAccountRequest
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/incompatible-input": func(t *testing.T) test {
|
||||
return test{
|
||||
nar: &NewAccountRequest{
|
||||
OnlyReturnExisting: true,
|
||||
Contact: []string{"foo", "bar"},
|
||||
},
|
||||
err: acme.MalformedErr(errors.Errorf("incompatible input; onlyReturnExisting must be alone")),
|
||||
}
|
||||
},
|
||||
"fail/bad-contact": func(t *testing.T) test {
|
||||
return test{
|
||||
nar: &NewAccountRequest{
|
||||
Contact: []string{"foo", ""},
|
||||
},
|
||||
err: acme.MalformedErr(errors.Errorf("contact cannot be empty string")),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
return test{
|
||||
nar: &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/onlyReturnExisting": func(t *testing.T) test {
|
||||
return test{
|
||||
nar: &NewAccountRequest{
|
||||
OnlyReturnExisting: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.nar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAccountRequestValidate(t *testing.T) {
|
||||
type test struct {
|
||||
uar *UpdateAccountRequest
|
||||
err *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/incompatible-input": func(t *testing.T) test {
|
||||
return test{
|
||||
uar: &UpdateAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
Status: "foo",
|
||||
},
|
||||
err: acme.MalformedErr(errors.Errorf("incompatible input; " +
|
||||
"contact and status updates are mutually exclusive")),
|
||||
}
|
||||
},
|
||||
"fail/bad-contact": func(t *testing.T) test {
|
||||
return test{
|
||||
uar: &UpdateAccountRequest{
|
||||
Contact: []string{"foo", ""},
|
||||
},
|
||||
err: acme.MalformedErr(errors.Errorf("contact cannot be empty string")),
|
||||
}
|
||||
},
|
||||
"fail/bad-status": func(t *testing.T) test {
|
||||
return test{
|
||||
uar: &UpdateAccountRequest{
|
||||
Status: "foo",
|
||||
},
|
||||
err: acme.MalformedErr(errors.Errorf("cannot update account " +
|
||||
"status to foo, only deactivated")),
|
||||
}
|
||||
},
|
||||
"ok/contact": func(t *testing.T) test {
|
||||
return test{
|
||||
uar: &UpdateAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
},
|
||||
}
|
||||
},
|
||||
"ok/status": func(t *testing.T) test {
|
||||
return test{
|
||||
uar: &UpdateAccountRequest{
|
||||
Status: "deactivated",
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := tc.uar.Validate(); err != nil {
|
||||
if assert.NotNil(t, err) {
|
||||
ae, ok := err.(*acme.Error)
|
||||
assert.True(t, ok)
|
||||
assert.HasPrefix(t, ae.Error(), tc.err.Error())
|
||||
assert.Equals(t, ae.StatusCode(), tc.err.StatusCode())
|
||||
assert.Equals(t, ae.Type, tc.err.Type)
|
||||
}
|
||||
} else {
|
||||
assert.Nil(t, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerGetOrdersByAccount(t *testing.T) {
|
||||
oids := []string{
|
||||
"https://ca.smallstep.com/acme/order/foo",
|
||||
"https://ca.smallstep.com/acme/order/bar",
|
||||
}
|
||||
accID := "account-id"
|
||||
prov := newProv()
|
||||
|
||||
// Request with chi context
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.URLParams.Add("accID", accID)
|
||||
url := fmt.Sprintf("http://ca.smallstep.com/acme/account/%s/orders", accID)
|
||||
|
||||
type test struct {
|
||||
auth acme.Interface
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
problem *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner": func(t *testing.T) test {
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{},
|
||||
ctx: context.Background(),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.Errorf("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-provisioner": func(t *testing.T) test {
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{},
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, nil),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.Errorf("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{},
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
||||
statusCode: 404,
|
||||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, nil)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{},
|
||||
ctx: ctx,
|
||||
statusCode: 404,
|
||||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
"fail/account-id-mismatch": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: "foo"}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{},
|
||||
ctx: ctx,
|
||||
statusCode: 401,
|
||||
problem: acme.UnauthorizedErr(errors.New("account ID does not match url param")),
|
||||
}
|
||||
},
|
||||
"fail/getOrdersByAccount-error": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: accID}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
err: acme.ServerInternalErr(errors.New("force")),
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("force")),
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
acc := &acme.Account{ID: accID}
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, acc)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
getOrdersByAccount: func(p provisioner.Interface, id string) ([]string, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, id, acc.ID)
|
||||
return oids, nil
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := New(tc.auth).(*Handler)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetOrdersByAccount(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
||||
var ae acme.AError
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
prob := tc.problem.ToACME()
|
||||
|
||||
assert.Equals(t, ae.Type, prob.Type)
|
||||
assert.Equals(t, ae.Detail, prob.Detail)
|
||||
assert.Equals(t, ae.Identifier, prob.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
expB, err := json.Marshal(oids)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerNewAccount(t *testing.T) {
|
||||
accID := "accountID"
|
||||
acc := acme.Account{
|
||||
ID: accID,
|
||||
Status: "valid",
|
||||
Orders: fmt.Sprintf("https://ca.smallstep.com/acme/account/%s/orders", accID),
|
||||
}
|
||||
prov := newProv()
|
||||
|
||||
url := "https://ca.smallstep.com/acme/new-account"
|
||||
|
||||
type test struct {
|
||||
auth acme.Interface
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
problem *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-provisioner": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/no-payload": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-payload": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
problem: acme.MalformedErr(errors.New("failed to unmarshal new-account request payload: unexpected end of JSON input")),
|
||||
}
|
||||
},
|
||||
"fail/malformed-payload-error": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", ""},
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
problem: acme.MalformedErr(errors.New("contact cannot be empty string")),
|
||||
}
|
||||
},
|
||||
"fail/no-existing-account": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
OnlyReturnExisting: true,
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 404,
|
||||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
"fail/no-jwk": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.Errorf("jwk expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-jwk": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.Errorf("jwk expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/NewAccount-error": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
newAccount: func(p provisioner.Interface, ops acme.AccountOptions) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, ops.Contact, nar.Contact)
|
||||
assert.Equals(t, ops.Key, jwk)
|
||||
return nil, acme.ServerInternalErr(errors.New("force"))
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("force")),
|
||||
}
|
||||
},
|
||||
"ok/new-account": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, jwkContextKey, jwk)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
newAccount: func(p provisioner.Interface, ops acme.AccountOptions) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, ops.Contact, nar.Contact)
|
||||
assert.Equals(t, ops.Key, jwk)
|
||||
return &acc, nil
|
||||
},
|
||||
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
||||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
||||
assert.Equals(t, typ, acme.AccountLink)
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{accID})
|
||||
return fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 201,
|
||||
}
|
||||
},
|
||||
"ok/return-existing": func(t *testing.T) test {
|
||||
nar := &NewAccountRequest{
|
||||
OnlyReturnExisting: true,
|
||||
}
|
||||
b, err := json.Marshal(nar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
||||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
||||
assert.Equals(t, typ, acme.AccountLink)
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{accID})
|
||||
return fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := New(tc.auth).(*Handler)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.NewAccount(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
||||
var ae acme.AError
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
prob := tc.problem.ToACME()
|
||||
|
||||
assert.Equals(t, ae.Type, prob.Type)
|
||||
assert.Equals(t, ae.Detail, prob.Detail)
|
||||
assert.Equals(t, ae.Identifier, prob.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
expB, err := json.Marshal(acc)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Location"],
|
||||
[]string{fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerGetUpdateAccount(t *testing.T) {
|
||||
accID := "accountID"
|
||||
acc := acme.Account{
|
||||
ID: accID,
|
||||
Status: "valid",
|
||||
Orders: fmt.Sprintf("https://ca.smallstep.com/acme/account/%s/orders", accID),
|
||||
}
|
||||
prov := newProv()
|
||||
|
||||
// Request with chi context
|
||||
url := fmt.Sprintf("http://ca.smallstep.com/acme/account/%s", accID)
|
||||
|
||||
type test struct {
|
||||
auth acme.Interface
|
||||
ctx context.Context
|
||||
statusCode int
|
||||
problem *acme.Error
|
||||
}
|
||||
var tests = map[string]func(t *testing.T) test{
|
||||
"fail/no-provisioner": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.Background(),
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-provisioner": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("provisioner expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/no-account": func(t *testing.T) test {
|
||||
return test{
|
||||
ctx: context.WithValue(context.Background(), provisionerContextKey, prov),
|
||||
statusCode: 404,
|
||||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
"fail/nil-account": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 404,
|
||||
problem: acme.AccountDoesNotExistErr(nil),
|
||||
}
|
||||
},
|
||||
"fail/no-payload": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/nil-payload": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, nil)
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
|
||||
}
|
||||
},
|
||||
"fail/unmarshal-payload-error": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
problem: acme.MalformedErr(errors.New("failed to unmarshal new-account request payload: unexpected end of JSON input")),
|
||||
}
|
||||
},
|
||||
"fail/malformed-payload-error": func(t *testing.T) test {
|
||||
uar := &UpdateAccountRequest{
|
||||
Contact: []string{"foo", ""},
|
||||
}
|
||||
b, err := json.Marshal(uar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
ctx: ctx,
|
||||
statusCode: 400,
|
||||
problem: acme.MalformedErr(errors.New("contact cannot be empty string")),
|
||||
}
|
||||
},
|
||||
"fail/Deactivate-error": func(t *testing.T) test {
|
||||
uar := &UpdateAccountRequest{
|
||||
Status: "deactivated",
|
||||
}
|
||||
b, err := json.Marshal(uar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
deactivateAccount: func(p provisioner.Interface, id string) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, id, accID)
|
||||
return nil, acme.ServerInternalErr(errors.New("force"))
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("force")),
|
||||
}
|
||||
},
|
||||
"fail/UpdateAccount-error": func(t *testing.T) test {
|
||||
uar := &UpdateAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(uar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
updateAccount: func(p provisioner.Interface, id string, contacts []string) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, id, accID)
|
||||
assert.Equals(t, contacts, uar.Contact)
|
||||
return nil, acme.ServerInternalErr(errors.New("force"))
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 500,
|
||||
problem: acme.ServerInternalErr(errors.New("force")),
|
||||
}
|
||||
},
|
||||
"ok/deactivate": func(t *testing.T) test {
|
||||
uar := &UpdateAccountRequest{
|
||||
Status: "deactivated",
|
||||
}
|
||||
b, err := json.Marshal(uar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
deactivateAccount: func(p provisioner.Interface, id string) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, id, accID)
|
||||
return &acc, nil
|
||||
},
|
||||
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
||||
assert.Equals(t, typ, acme.AccountLink)
|
||||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{accID})
|
||||
return fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/new-account": func(t *testing.T) test {
|
||||
uar := &UpdateAccountRequest{
|
||||
Contact: []string{"foo", "bar"},
|
||||
}
|
||||
b, err := json.Marshal(uar)
|
||||
assert.FatalError(t, err)
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b})
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
updateAccount: func(p provisioner.Interface, id string, contacts []string) (*acme.Account, error) {
|
||||
assert.Equals(t, p, prov)
|
||||
assert.Equals(t, id, accID)
|
||||
assert.Equals(t, contacts, uar.Contact)
|
||||
return &acc, nil
|
||||
},
|
||||
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
||||
assert.Equals(t, typ, acme.AccountLink)
|
||||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{accID})
|
||||
return fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
"ok/post-as-get": func(t *testing.T) test {
|
||||
ctx := context.WithValue(context.Background(), provisionerContextKey, prov)
|
||||
ctx = context.WithValue(ctx, accContextKey, &acc)
|
||||
ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true})
|
||||
return test{
|
||||
auth: &mockAcmeAuthority{
|
||||
getLink: func(typ acme.Link, provID string, abs bool, in ...string) string {
|
||||
assert.Equals(t, typ, acme.AccountLink)
|
||||
assert.Equals(t, provID, acme.URLSafeProvisionerName(prov))
|
||||
assert.True(t, abs)
|
||||
assert.Equals(t, in, []string{accID})
|
||||
return fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
statusCode: 200,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
tc := run(t)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := New(tc.auth).(*Handler)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req = req.WithContext(tc.ctx)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetUpdateAccount(w, req)
|
||||
res := w.Result()
|
||||
|
||||
assert.Equals(t, res.StatusCode, tc.statusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
|
||||
var ae acme.AError
|
||||
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
|
||||
prob := tc.problem.ToACME()
|
||||
|
||||
assert.Equals(t, ae.Type, prob.Type)
|
||||
assert.Equals(t, ae.Detail, prob.Detail)
|
||||
assert.Equals(t, ae.Identifier, prob.Identifier)
|
||||
assert.Equals(t, ae.Subproblems, prob.Subproblems)
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
|
||||
} else {
|
||||
expB, err := json.Marshal(acc)
|
||||
assert.FatalError(t, err)
|
||||
assert.Equals(t, bytes.TrimSpace(body), expB)
|
||||
assert.Equals(t, res.Header["Location"],
|
||||
[]string{fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/%s",
|
||||
acme.URLSafeProvisionerName(prov), accID)})
|
||||
assert.Equals(t, res.Header["Content-Type"], []string{"application/json"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue