Pulling out sa module

This commit is contained in:
Richard Barnes 2015-03-10 15:21:50 -07:00
parent e8126fd390
commit dcdf9954ae
6 changed files with 261 additions and 245 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/letsencrypt/boulder"
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/ra"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/va"
"github.com/letsencrypt/boulder/wfe"
)
@ -109,7 +110,7 @@ func main() {
// Create the components
wfe := wfe.NewWebFrontEndImpl()
sa, err := boulder.NewSQLStorageAuthority("sqlite3", ":memory:")
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
failOnError(err, "Unable to create SA")
ra := ra.NewRegistrationAuthorityImpl()
va := va.NewValidationAuthorityImpl()
@ -173,7 +174,7 @@ func main() {
failOnError(err, "Failed to create VA server")
ras, err := boulder.NewRegistrationAuthorityServer("RA.server", ch, &vac, &cac, &sac)
failOnError(err, "Failed to create RA server")
sai, err := boulder.NewSQLStorageAuthority("sqlite3", ":memory:")
sai, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
failOnError(err, "Failed to create SA impl")
sas := boulder.NewStorageAuthorityServer("SA.server", ch, sai)
@ -261,7 +262,7 @@ func main() {
Action: func(c *cli.Context) {
ch := amqpChannel(c.GlobalString("amqp"))
sai, err := boulder.NewSQLStorageAuthority("sqlite3", ":memory:")
sai, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
failOnError(err, "Failed to create SA impl")
sas := boulder.NewStorageAuthorityServer("SA.server", ch, sai)
runForever(sas)

View File

@ -13,6 +13,9 @@ import (
"github.com/cloudflare/cfssl/signer/local"
_ "github.com/mattn/go-sqlite3"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
)
var CA_KEY_PEM = "-----BEGIN RSA PRIVATE KEY-----\n" +
@ -139,31 +142,31 @@ func TestIssueCertificate(t *testing.T) {
signer, _ := local.NewSigner(caKey, caCert, x509.SHA256WithRSA, nil)
// Create an SA
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
AssertNotError(t, err, "Failed to create SA")
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
// Create a CA
/*
// Uncomment to test with a remote signer
ca, err := NewCertificateAuthorityImpl("localhost:9000", "79999d86250c367a2b517a1ae7d409c1", "ee")
AssertNotError(t, err, "Failed to create CA")
test.AssertNotError(t, err, "Failed to create CA")
ca.SA = sa
*/
ca := CertificateAuthorityImpl{
signer: signer,
Signer: signer,
SA: sa,
}
// Sign CSR
certObj, err := ca.IssueCertificate(*csr)
AssertNotError(t, err, "Failed to sign certificate")
test.AssertNotError(t, err, "Failed to sign certificate")
// Verify cert contents
cert, err := x509.ParseCertificate(certObj.DER)
AssertNotError(t, err, "Certificate failed to parse")
test.AssertNotError(t, err, "Certificate failed to parse")
AssertEquals(t, cert.Subject.CommonName, "example.com")
test.AssertEquals(t, cert.Subject.CommonName, "example.com")
if len(cert.DNSNames) != 2 || cert.DNSNames[0] != "example.com" || cert.DNSNames[1] != "www.example.com" {
t.Errorf("Improper list of domain names %v", cert.DNSNames)
@ -171,5 +174,5 @@ func TestIssueCertificate(t *testing.T) {
// Verify that the cert got stored in the DB
_, err = sa.GetCertificate(certObj.ID)
AssertNotError(t, err, "Certificate not found in database")
test.AssertNotError(t, err, "Certificate not found in database")
}

View File

@ -6,271 +6,246 @@
package boulder
import (
"bufio"
"strings"
"fmt"
"net"
"testing"
"time"
"bufio"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/letsencrypt/boulder/test"
)
const TimeoutIndicator = "<TIMEOUT>"
func AssertNotError(t *testing.T, err error, message string) {
if err != nil {
t.Error(message, err)
}
}
func AssertEquals(t *testing.T, one string, two string) {
if one != two {
t.Errorf("String [%s] != [%s]", one, two)
}
}
func AssertContains(t *testing.T, haystack string, needle string) {
if ! strings.Contains(haystack, needle) {
t.Errorf("String [%s] does not contain [%s]", haystack, needle)
}
}
func AssertSeverity(t *testing.T, data string, severity int) {
expected := fmt.Sprintf("\"severity\":%d", severity)
AssertContains(t, data, expected)
}
func readChanWithTimeout(outChan <-chan string) string {
timeout := time.After(time.Second)
timeout := time.After(time.Second)
select {
case line := <-outChan:
return line
case <-timeout:
return TimeoutIndicator
}
select {
case line := <-outChan:
return line
case <-timeout:
return TimeoutIndicator
}
}
func awaitMessage(t *testing.T, scheme string, address string) (net.Listener, <-chan string) {
outChan := make(chan string)
func awaitMessage(t *testing.T, scheme string, address string) (net.Listener, <-chan string) {
outChan := make(chan string)
socket, err := net.Listen(scheme, address)
AssertNotError(t, err, "Could not listen")
socket, err := net.Listen(scheme, address)
test.AssertNotError(t, err, "Could not listen")
recvLog := func() {
conn, err := socket.Accept()
recvLog := func() {
conn, err := socket.Accept()
if conn == nil {
t.Error("Conn nil; programmer error in test.")
return
}
if conn == nil {
t.Error("Conn nil; programmer error in test.")
return
}
defer func(){
conn.Close()
fmt.Println("Exiting")
}()
defer func() {
conn.Close()
fmt.Println("Exiting")
}()
AssertNotError(t, err, "Could not accept")
test.AssertNotError(t, err, "Could not accept")
reader := bufio.NewReader(conn)
reader := bufio.NewReader(conn)
for {
conn.SetDeadline(time.Now().Add(time.Second))
line, _ := reader.ReadString('\n')
for {
conn.SetDeadline(time.Now().Add(time.Second))
line, _ := reader.ReadString('\n')
// Emit the line if it's not-empty.
if line != "" {
outChan <- line
}
}
// Emit the line if it's not-empty.
if line != "" {
outChan <- line
}
}
}
}
go recvLog()
go recvLog()
// Let the caller close the socket
return socket, outChan
// Let the caller close the socket
return socket, outChan
}
func TestWriteTcp(t *testing.T) {
const Scheme = "tcp"
const Address = "127.0.0.1:9999"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
const Scheme = "tcp"
const Address = "127.0.0.1:9999"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
msg := "Test " + Scheme + " " + Address
log.Critical(msg, nil)
msg := "Test " + Scheme + " " + Address
log.Critical(msg, nil)
rsp := <-outChan
AssertContains(t, rsp, msg)
AssertSeverity(t, rsp, CRITICAL)
rsp := <-outChan
test.AssertContains(t, rsp, msg)
test.AssertSeverity(t, rsp, CRITICAL)
}
func TestWriteNoNetwork(t *testing.T) {
log := NewJsonLogger("just a test")
log.Debug("Check", nil)
// Nothing to assert
log := NewJsonLogger("just a test")
log.Debug("Check", nil)
// Nothing to assert
log.EnableStdOut(true)
log.Debug("Check", nil)
// Nothing to assert
log.EnableStdOut(true)
log.Debug("Check", nil)
// Nothing to assert
}
func TestWriteUnMarshallable(t *testing.T) {
const Scheme = "tcp"
const Address = "127.0.0.1:9998"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
const Scheme = "tcp"
const Address = "127.0.0.1:9998"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
log := NewJsonLogger("please don't work")
log.SetEndpoint(Scheme, Address)
log.Connect()
log := NewJsonLogger("please don't work")
log.SetEndpoint(Scheme, Address)
log.Connect()
log.Debug("Check", func(){})
rsp := readChanWithTimeout(outChan)
log.Debug("Check", func() {})
rsp := readChanWithTimeout(outChan)
AssertEquals(t, rsp, TimeoutIndicator)
test.AssertEquals(t, rsp, TimeoutIndicator)
}
func TestWriteTcpAllLevels(t *testing.T) {
const Scheme = "tcp"
const Address = "127.0.0.1:9997"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
const Scheme = "tcp"
const Address = "127.0.0.1:9997"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
msg := "Test " + Scheme + " " + Address
{
log.Critical(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, CRITICAL)
AssertContains(t, rsp, msg)
}
msg := "Test " + Scheme + " " + Address
{
log.Critical(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, CRITICAL)
test.AssertContains(t, rsp, msg)
}
{
log.Alert(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, ALERT)
AssertContains(t, rsp, msg)
}
{
log.Alert(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, ALERT)
test.AssertContains(t, rsp, msg)
}
{
log.Emergency(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, EMERGENCY)
AssertContains(t, rsp, msg)
}
{
log.Emergency(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, EMERGENCY)
test.AssertContains(t, rsp, msg)
}
{
log.Error(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, ERROR)
AssertContains(t, rsp, msg)
}
{
log.Error(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, ERROR)
test.AssertContains(t, rsp, msg)
}
{
log.Warning(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, WARNING)
AssertContains(t, rsp, msg)
}
{
log.Warning(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, WARNING)
test.AssertContains(t, rsp, msg)
}
{
log.Notice(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, NOTICE)
AssertContains(t, rsp, msg)
}
{
log.Notice(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, NOTICE)
test.AssertContains(t, rsp, msg)
}
{
log.Info(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, INFO)
AssertContains(t, rsp, msg)
}
{
log.Info(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, INFO)
test.AssertContains(t, rsp, msg)
}
{
log.Debug(msg, msg)
rsp := <-outChan
AssertSeverity(t, rsp, DEBUG)
AssertContains(t, rsp, msg)
}
{
log.Debug(msg, msg)
rsp := <-outChan
test.AssertSeverity(t, rsp, DEBUG)
test.AssertContains(t, rsp, msg)
}
}
func TestLevelMasking(t *testing.T) {
const Scheme = "tcp"
const Address = "127.0.0.1:9996"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
const Scheme = "tcp"
const Address = "127.0.0.1:9996"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
log := NewJsonLogger("just a test")
log.SetEndpoint(Scheme, Address)
msg := "Test " + Scheme + " " + Address
msg := "Test " + Scheme + " " + Address
{
log.Info(msg, msg)
rsp := readChanWithTimeout(outChan)
AssertSeverity(t, rsp, INFO)
AssertContains(t, rsp, msg)
}
{
log.Info(msg, msg)
rsp := readChanWithTimeout(outChan)
test.AssertSeverity(t, rsp, INFO)
test.AssertContains(t, rsp, msg)
}
// Notice and lower numbers should emit; Info should not.
log.SetLevel(NOTICE)
// Notice and lower numbers should emit; Info should not.
log.SetLevel(NOTICE)
{
log.Info(msg, msg)
rsp := readChanWithTimeout(outChan)
AssertEquals(t, rsp, TimeoutIndicator)
}
{
log.Info(msg, msg)
rsp := readChanWithTimeout(outChan)
test.AssertEquals(t, rsp, TimeoutIndicator)
}
// Warning, being lower than Notice, should emit.
{
log.Warning(msg, msg)
// Warning, being lower than Notice, should emit.
{
log.Warning(msg, msg)
rsp := readChanWithTimeout(outChan)
AssertSeverity(t, rsp, WARNING)
AssertContains(t, rsp, msg)
}
rsp := readChanWithTimeout(outChan)
test.AssertSeverity(t, rsp, WARNING)
test.AssertContains(t, rsp, msg)
}
}
func TestEmbeddedNewline(t *testing.T) {
const Scheme = "tcp"
const Address = "127.0.0.1:9995"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
const Scheme = "tcp"
const Address = "127.0.0.1:9995"
socket, outChan := awaitMessage(t, Scheme, Address)
defer socket.Close()
log := NewJsonLogger("embedded newline")
log.SetEndpoint(Scheme, Address)
log := NewJsonLogger("embedded newline")
log.SetEndpoint(Scheme, Address)
payload := struct {
One string
Two string
}{
One: "A\nTOYOTA'S\nA\nTOYOTA",
Two: "\n\n\n\n\n",
}
payload := struct {
One string
Two string
}{
One: "A\nTOYOTA'S\nA\nTOYOTA",
Two: "\n\n\n\n\n",
}
msg := "There's a newline in the payload:"
log.Critical(msg, payload)
msg := "There's a newline in the payload:"
log.Critical(msg, payload)
rsp := <-outChan
test.AssertContains(t, rsp, msg)
test.AssertSeverity(t, rsp, CRITICAL)
rsp := <-outChan
AssertContains(t, rsp, msg)
AssertSeverity(t, rsp, CRITICAL)
// I can't do an AssertContains directly because rsp is escaped, while the
// payload values are not. Since escaping routines are not so easy to find,
// payload I can't just JSON-marshal (because that is a loopback test),
// I do it manually.
AssertContains(t, rsp, strings.Replace(payload.One, "\n", "\\n", -1))
AssertContains(t, rsp, strings.Replace(payload.Two, "\n", "\\n", -1))
// I can't do an test.AssertContains directly because rsp is escaped, while the
// payload values are not. Since escaping routines are not so easy to find,
// payload I can't just JSON-marshal (because that is a loopback test),
// I do it manually.
test.AssertContains(t, rsp, strings.Replace(payload.One, "\n", "\\n", -1))
test.AssertContains(t, rsp, strings.Replace(payload.Two, "\n", "\\n", -1))
}

View File

@ -20,6 +20,8 @@ import (
"github.com/letsencrypt/boulder/ca"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
)
func TestForbiddenIdentifier(t *testing.T) {
@ -139,12 +141,12 @@ var (
AuthzFinalWWW = core.Authorization{}
)
func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationAuthority, *SQLStorageAuthority, core.RegistrationAuthority) {
func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationAuthority, *sa.SQLStorageAuthority, core.RegistrationAuthority) {
err := json.Unmarshal(AccountKeyJSON, &AccountKey)
AssertNotError(t, err, "Failed to unmarshall JWK")
test.AssertNotError(t, err, "Failed to unmarshall JWK")
sa, err := NewSQLStorageAuthority("sqlite3", ":memory:")
AssertNotError(t, err, "Failed to create SA")
sa, err := sa.NewSQLStorageAuthority("sqlite3", ":memory:")
test.AssertNotError(t, err, "Failed to create SA")
sa.InitTables()
va := &DummyValidationAuthority{}
@ -167,17 +169,11 @@ func initAuthorities(t *testing.T) (core.CertificateAuthority, *DummyValidationA
return &ca, va, sa, &ra
}
func assert(t *testing.T, test bool, message string) {
if !test {
t.Error(message)
}
}
func assertAuthzEqual(t *testing.T, a1, a2 core.Authorization) {
assert(t, a1.ID == a2.ID, "ret != DB: ID")
assert(t, a1.Identifier == a2.Identifier, "ret != DB: Identifier")
assert(t, a1.Status == a2.Status, "ret != DB: Status")
assert(t, a1.Key.Equals(a2.Key), "ret != DB: Key")
test.Assert(t, a1.ID == a2.ID, "ret != DB: ID")
test.Assert(t, a1.Identifier == a2.Identifier, "ret != DB: Identifier")
test.Assert(t, a1.Status == a2.Status, "ret != DB: Status")
test.Assert(t, a1.Key.Equals(a2.Key), "ret != DB: Key")
// Not testing: Contact, Challenges
}
@ -185,22 +181,22 @@ func TestNewAuthorization(t *testing.T) {
_, _, sa, ra := initAuthorities(t)
authz, err := ra.NewAuthorization(AuthzRequest, AccountKey)
AssertNotError(t, err, "NewAuthorization failed")
test.AssertNotError(t, err, "NewAuthorization failed")
// Verify that returned authz same as DB
dbAuthz, err := sa.GetAuthorization(authz.ID)
AssertNotError(t, err, "Could not fetch authorization from database")
test.AssertNotError(t, err, "Could not fetch authorization from database")
assertAuthzEqual(t, authz, dbAuthz)
// Verify that the returned authz has the right information
assert(t, authz.Key.Equals(AccountKey), "Initial authz did not get the right key")
assert(t, authz.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier")
assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
test.Assert(t, authz.Key.Equals(AccountKey), "Initial authz did not get the right key")
test.Assert(t, authz.Identifier == AuthzRequest.Identifier, "Initial authz had wrong identifier")
test.Assert(t, authz.Status == core.StatusPending, "Initial authz not pending")
_, ok := authz.Challenges[core.ChallengeTypeDVSNI]
assert(t, ok, "Initial authz does not include DVSNI challenge")
test.Assert(t, ok, "Initial authz does not include DVSNI challenge")
_, ok = authz.Challenges[core.ChallengeTypeSimpleHTTPS]
assert(t, ok, "Initial authz does not include SimpleHTTPS challenge")
test.Assert(t, ok, "Initial authz does not include SimpleHTTPS challenge")
// If we get to here, we'll use this authorization for the next test
AuthzInitial = authz
@ -216,26 +212,26 @@ func TestUpdateAuthorization(t *testing.T) {
AuthzDelta.ID = AuthzInitial.ID
authz, err := ra.UpdateAuthorization(AuthzDelta)
AssertNotError(t, err, "UpdateAuthorization failed")
test.AssertNotError(t, err, "UpdateAuthorization failed")
// Verify that returned authz same as DB
dbAuthz, err := sa.GetAuthorization(authz.ID)
AssertNotError(t, err, "Could not fetch authorization from database")
test.AssertNotError(t, err, "Could not fetch authorization from database")
assertAuthzEqual(t, authz, dbAuthz)
// Verify that the VA got the authz, and it's the same as the others
assert(t, va.Called, "Authorization was not passed to the VA")
test.Assert(t, va.Called, "Authorization was not passed to the VA")
assertAuthzEqual(t, authz, va.Argument)
// Verify that the responses are reflected
simpleHttps, ok := va.Argument.Challenges[core.ChallengeTypeSimpleHTTPS]
simpleHttpsOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeSimpleHTTPS]
assert(t, ok, "Authz passed to VA has no simpleHttps challenge")
assert(t, simpleHttps.Path == simpleHttpsOrig.Path, "simpleHttps changed")
test.Assert(t, ok, "Authz passed to VA has no simpleHttps challenge")
test.Assert(t, simpleHttps.Path == simpleHttpsOrig.Path, "simpleHttps changed")
dvsni, ok := va.Argument.Challenges[core.ChallengeTypeDVSNI]
dvsniOrig, _ := AuthzDelta.Challenges[core.ChallengeTypeDVSNI]
assert(t, ok, "Authz passed to VA has no dvsni challenge")
assert(t, dvsni.Token == dvsniOrig.Token, "dvsni changed")
test.Assert(t, ok, "Authz passed to VA has no dvsni challenge")
test.Assert(t, dvsni.Token == dvsniOrig.Token, "dvsni changed")
// If we get to here, we'll use this authorization for the next test
AuthzUpdated = authz
@ -260,7 +256,7 @@ func TestOnValidationUpdate(t *testing.T) {
// Verify that the Authz in the DB is the same except for Status->StatusValid
AuthzFromVA.Status = core.StatusValid
dbAuthz, err := sa.GetAuthorization(AuthzFromVA.ID)
AssertNotError(t, err, "Could not fetch authorization from database")
test.AssertNotError(t, err, "Could not fetch authorization from database")
assertAuthzEqual(t, AuthzFromVA, dbAuthz)
t.Log(" ~~> from VA: ", AuthzFromVA.Status)
t.Log(" ~~> from DB: ", dbAuthz.Status)
@ -293,12 +289,12 @@ func TestNewCertificate(t *testing.T) {
}
cert, err := ra.NewCertificate(certRequest, AccountKey)
AssertNotError(t, err, "Failed to issue certificate")
test.AssertNotError(t, err, "Failed to issue certificate")
// Verify that cert shows up and is as expected
dbCert, err := sa.GetCertificate(cert.ID)
AssertNotError(t, err, "Could not fetch certificate from database")
assert(t, bytes.Compare(cert.DER, dbCert) == 0, "Certificates differ")
test.AssertNotError(t, err, "Could not fetch certificate from database")
test.Assert(t, bytes.Compare(cert.DER, dbCert) == 0, "Certificates differ")
// TODO Test failure cases
t.Log("DONE TestOnValidationUpdate")

View File

@ -3,7 +3,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package boulder
package sa
import (
"crypto/sha256"

41
test/test-tools.go Normal file
View File

@ -0,0 +1,41 @@
// Copyright 2014 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package test
import (
"fmt"
"strings"
"testing"
)
func Assert(t *testing.T, result bool, message string) {
if !result {
t.Error(message)
}
}
func AssertNotError(t *testing.T, err error, message string) {
if err != nil {
t.Error(message, err)
}
}
func AssertEquals(t *testing.T, one string, two string) {
if one != two {
t.Errorf("String [%s] != [%s]", one, two)
}
}
func AssertContains(t *testing.T, haystack string, needle string) {
if !strings.Contains(haystack, needle) {
t.Errorf("String [%s] does not contain [%s]", haystack, needle)
}
}
func AssertSeverity(t *testing.T, data string, severity int) {
expected := fmt.Sprintf("\"severity\":%d", severity)
AssertContains(t, data, expected)
}