Allow specifying different initial and resumption expectations.

We have an ad-hoc expectResumeVersion, and I'll want to add one for ALPS
(to confirm changing ALPS on resumption works). We probably could have
done with such a test for ALPN too. Wrap all the ConnectionState
expectations into a struct with an optional resumeExpectations field.

Also move the OCSPResponse() method to ConnectionState for consistency.

Change-Id: Icaabf5571c51e78ed078f57de0e04928d3f3fa8d
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/43084
Reviewed-by: Steven Valdez <svaldez@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 6a744db..d1ea08d 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -249,6 +249,7 @@
 	ServerName                 string                // server name requested by client, if any (server side only)
 	PeerCertificates           []*x509.Certificate   // certificate chain presented by remote peer
 	VerifiedChains             [][]*x509.Certificate // verified chains built from PeerCertificates
+	OCSPResponse               []byte                // stapled OCSP response from the peer, if any
 	ChannelID                  *ecdsa.PublicKey      // the channel ID for this connection
 	TokenBindingNegotiated     bool                  // whether Token Binding was negotiated
 	TokenBindingParam          uint8                 // the negotiated Token Binding key parameter
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index a449f2f..04fe16c 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -1872,6 +1872,7 @@
 		state.CipherSuite = c.cipherSuite.id
 		state.PeerCertificates = c.peerCertificates
 		state.VerifiedChains = c.verifiedChains
+		state.OCSPResponse = c.ocspResponse
 		state.ServerName = c.serverName
 		state.ChannelID = c.channelID
 		state.TokenBindingNegotiated = c.tokenBindingNegotiated
@@ -1887,15 +1888,6 @@
 	return state
 }
 
-// OCSPResponse returns the stapled OCSP response from the TLS server, if
-// any. (Only valid for client connections.)
-func (c *Conn) OCSPResponse() []byte {
-	c.handshakeMutex.Lock()
-	defer c.handshakeMutex.Unlock()
-
-	return c.ocspResponse
-}
-
 // VerifyHostname checks that the peer certificate chain is valid for
 // connecting to host.  If so, it returns nil; if not, it returns an error
 // describing the problem.
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 2bf8d13..4baf901 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -491,6 +491,50 @@
 	npn  = 2
 )
 
+// connectionExpectations contains connection-level test expectations to check
+// on the runner side.
+type connectionExpectations struct {
+	// version, if non-zero, specifies the TLS version that must be negotiated.
+	version uint16
+	// cipher, if non-zero, specifies the TLS cipher suite that should be
+	// negotiated.
+	cipher uint16
+	// channelID controls whether the connection should have negotiated a
+	// Channel ID with channelIDKey.
+	channelID bool
+	// tokenBinding controls whether the connection should have negotiated Token
+	// Binding.
+	tokenBinding bool
+	// tokenBindingParam is the Token Binding parameter that should have been
+	// negotiated (if tokenBinding is true).
+	tokenBindingParam uint8
+	// nextProto controls whether the connection should negotiate a next
+	// protocol via NPN or ALPN.
+	nextProto string
+	// noNextProto, if true, means that no next protocol should be negotiated.
+	noNextProto bool
+	// nextProtoType, if non-zero, is the next protocol negotiation mechanism.
+	nextProtoType int
+	// srtpProtectionProfile is the DTLS-SRTP profile that should be negotiated.
+	// If zero, none should be negotiated.
+	srtpProtectionProfile uint16
+	// ocspResponse, if not nil, is the OCSP response to be received.
+	ocspResponse []uint8
+	// sctList, if not nil, is the SCT list to be received.
+	sctList []uint8
+	// peerSignatureAlgorithm, if not zero, is the signature algorithm that the
+	// peer should have used in the handshake.
+	peerSignatureAlgorithm signatureAlgorithm
+	// curveID, if not zero, is the curve that the handshake should have used.
+	curveID CurveID
+	// peerCertificate, if not nil, is the certificate chain the peer is
+	// expected to send.
+	peerCertificate *Certificate
+	// quicTransportParams contains the QUIC transport parameters that are to be
+	// sent by the peer.
+	quicTransportParams []byte
+}
+
 type testCase struct {
 	testType      testType
 	protocol      protocol
@@ -501,46 +545,12 @@
 	// expectedLocalError, if not empty, contains a substring that must be
 	// found in the local error.
 	expectedLocalError string
-	// expectedVersion, if non-zero, specifies the TLS version that must be
-	// negotiated.
-	expectedVersion uint16
-	// expectedResumeVersion, if non-zero, specifies the TLS version that
-	// must be negotiated on resumption. If zero, expectedVersion is used.
-	expectedResumeVersion uint16
-	// expectedCipher, if non-zero, specifies the TLS cipher suite that
-	// should be negotiated.
-	expectedCipher uint16
-	// expectChannelID controls whether the connection should have
-	// negotiated a Channel ID with channelIDKey.
-	expectChannelID bool
-	// expectTokenBinding controls whether the connection should have
-	// negotiated Token Binding.
-	expectTokenBinding bool
-	// expectedTokenBindingParam is the Token Binding parameter that should
-	// have been negotiated (if expectTokenBinding is true).
-	expectedTokenBindingParam uint8
-	// expectedNextProto controls whether the connection should
-	// negotiate a next protocol via NPN or ALPN.
-	expectedNextProto string
-	// expectNoNextProto, if true, means that no next protocol should be
-	// negotiated.
-	expectNoNextProto bool
-	// expectedNextProtoType, if non-zero, is the expected next
-	// protocol negotiation mechanism.
-	expectedNextProtoType int
-	// expectedSRTPProtectionProfile is the DTLS-SRTP profile that
-	// should be negotiated. If zero, none should be negotiated.
-	expectedSRTPProtectionProfile uint16
-	// expectedOCSPResponse, if not nil, is the expected OCSP response to be received.
-	expectedOCSPResponse []uint8
-	// expectedSCTList, if not nil, is the expected SCT list to be received.
-	expectedSCTList []uint8
-	// expectedPeerSignatureAlgorithm, if not zero, is the signature
-	// algorithm that the peer should have used in the handshake.
-	expectedPeerSignatureAlgorithm signatureAlgorithm
-	// expectedCurveID, if not zero, is the curve that the handshake should
-	// have used.
-	expectedCurveID CurveID
+	// expectations contains test expectations for the initial
+	// connection.
+	expectations connectionExpectations
+	// resumeExpectations, if non-nil, contains test expectations for the
+	// resumption connection. If nil, |expectations| is used.
+	resumeExpectations *connectionExpectations
 	// messageLen is the length, in bytes, of the test message that will be
 	// sent.
 	messageLen int
@@ -639,17 +649,11 @@
 	// expectMessageDropped, if true, means the test message is expected to
 	// be dropped by the client rather than echoed back.
 	expectMessageDropped bool
-	// expectPeerCertificate, if not nil, is the certificate chain the peer
-	// is expected to send.
-	expectPeerCertificate *Certificate
 	// shimPrefix is the prefix that the shim will send to the server.
 	shimPrefix string
 	// resumeShimPrefix is the prefix that the shim will send to the server on a
 	// resumption.
 	resumeShimPrefix string
-	// expectedQUICTransportParams contains the QUIC transport
-	// parameters that are expected to be sent by the peer.
-	expectedQUICTransportParams []byte
 	// exportTrafficSecrets, if true, configures the test to export the TLS 1.3
 	// traffic secrets and confirms that they match.
 	exportTrafficSecrets bool
@@ -819,26 +823,23 @@
 		return err
 	}
 
-	// TODO(davidben): move all per-connection expectations into a dedicated
-	// expectations struct that can be specified separately for the two
-	// legs.
-	expectedVersion := test.expectedVersion
-	if isResume && test.expectedResumeVersion != 0 {
-		expectedVersion = test.expectedResumeVersion
+	expectations := &test.expectations
+	if isResume && test.resumeExpectations != nil {
+		expectations = test.resumeExpectations
 	}
 	connState := tlsConn.ConnectionState()
-	if vers := connState.Version; expectedVersion != 0 && vers != expectedVersion {
-		return fmt.Errorf("got version %x, expected %x", vers, expectedVersion)
+	if vers := connState.Version; expectations.version != 0 && vers != expectations.version {
+		return fmt.Errorf("got version %x, expected %x", vers, expectations.version)
 	}
 
-	if cipher := connState.CipherSuite; test.expectedCipher != 0 && cipher != test.expectedCipher {
-		return fmt.Errorf("got cipher %x, expected %x", cipher, test.expectedCipher)
+	if cipher := connState.CipherSuite; expectations.cipher != 0 && cipher != expectations.cipher {
+		return fmt.Errorf("got cipher %x, expected %x", cipher, expectations.cipher)
 	}
 	if didResume := connState.DidResume; isResume && didResume == test.expectResumeRejected {
 		return fmt.Errorf("didResume is %t, but we expected the opposite", didResume)
 	}
 
-	if test.expectChannelID {
+	if expectations.channelID {
 		channelID := connState.ChannelID
 		if channelID == nil {
 			return fmt.Errorf("no channel ID negotiated")
@@ -852,68 +853,68 @@
 		return fmt.Errorf("channel ID unexpectedly negotiated")
 	}
 
-	if test.expectTokenBinding {
+	if expectations.tokenBinding {
 		if !connState.TokenBindingNegotiated {
 			return errors.New("no Token Binding negotiated")
 		}
-		if connState.TokenBindingParam != test.expectedTokenBindingParam {
-			return fmt.Errorf("expected param %02x, but got %02x", test.expectedTokenBindingParam, connState.TokenBindingParam)
+		if connState.TokenBindingParam != expectations.tokenBindingParam {
+			return fmt.Errorf("expected param %02x, but got %02x", expectations.tokenBindingParam, connState.TokenBindingParam)
 		}
 	} else if connState.TokenBindingNegotiated {
 		return errors.New("Token Binding unexpectedly negotiated")
 	}
 
-	if expected := test.expectedNextProto; expected != "" {
+	if expected := expectations.nextProto; expected != "" {
 		if actual := connState.NegotiatedProtocol; actual != expected {
 			return fmt.Errorf("next proto mismatch: got %s, wanted %s", actual, expected)
 		}
 	}
 
-	if test.expectNoNextProto {
+	if expectations.noNextProto {
 		if actual := connState.NegotiatedProtocol; actual != "" {
 			return fmt.Errorf("got unexpected next proto %s", actual)
 		}
 	}
 
-	if test.expectedNextProtoType != 0 {
-		if (test.expectedNextProtoType == alpn) != connState.NegotiatedProtocolFromALPN {
+	if expectations.nextProtoType != 0 {
+		if (expectations.nextProtoType == alpn) != connState.NegotiatedProtocolFromALPN {
 			return fmt.Errorf("next proto type mismatch")
 		}
 	}
 
-	if p := connState.SRTPProtectionProfile; p != test.expectedSRTPProtectionProfile {
-		return fmt.Errorf("SRTP profile mismatch: got %d, wanted %d", p, test.expectedSRTPProtectionProfile)
+	if p := connState.SRTPProtectionProfile; p != expectations.srtpProtectionProfile {
+		return fmt.Errorf("SRTP profile mismatch: got %d, wanted %d", p, expectations.srtpProtectionProfile)
 	}
 
-	if test.expectedOCSPResponse != nil && !bytes.Equal(test.expectedOCSPResponse, tlsConn.OCSPResponse()) {
-		return fmt.Errorf("OCSP Response mismatch: got %x, wanted %x", tlsConn.OCSPResponse(), test.expectedOCSPResponse)
+	if expectations.ocspResponse != nil && !bytes.Equal(expectations.ocspResponse, connState.OCSPResponse) {
+		return fmt.Errorf("OCSP Response mismatch: got %x, wanted %x", connState.OCSPResponse, expectations.ocspResponse)
 	}
 
-	if test.expectedSCTList != nil && !bytes.Equal(test.expectedSCTList, connState.SCTList) {
+	if expectations.sctList != nil && !bytes.Equal(expectations.sctList, connState.SCTList) {
 		return fmt.Errorf("SCT list mismatch")
 	}
 
-	if expected := test.expectedPeerSignatureAlgorithm; expected != 0 && expected != connState.PeerSignatureAlgorithm {
+	if expected := expectations.peerSignatureAlgorithm; expected != 0 && expected != connState.PeerSignatureAlgorithm {
 		return fmt.Errorf("expected peer to use signature algorithm %04x, but got %04x", expected, connState.PeerSignatureAlgorithm)
 	}
 
-	if expected := test.expectedCurveID; expected != 0 && expected != connState.CurveID {
+	if expected := expectations.curveID; expected != 0 && expected != connState.CurveID {
 		return fmt.Errorf("expected peer to use curve %04x, but got %04x", expected, connState.CurveID)
 	}
 
-	if test.expectPeerCertificate != nil {
-		if len(connState.PeerCertificates) != len(test.expectPeerCertificate.Certificate) {
-			return fmt.Errorf("expected peer to send %d certificates, but got %d", len(connState.PeerCertificates), len(test.expectPeerCertificate.Certificate))
+	if expectations.peerCertificate != nil {
+		if len(connState.PeerCertificates) != len(expectations.peerCertificate.Certificate) {
+			return fmt.Errorf("expected peer to send %d certificates, but got %d", len(connState.PeerCertificates), len(expectations.peerCertificate.Certificate))
 		}
 		for i, cert := range connState.PeerCertificates {
-			if !bytes.Equal(cert.Raw, test.expectPeerCertificate.Certificate[i]) {
+			if !bytes.Equal(cert.Raw, expectations.peerCertificate.Certificate[i]) {
 				return fmt.Errorf("peer certificate %d did not match", i+1)
 			}
 		}
 	}
 
-	if len(test.expectedQUICTransportParams) > 0 {
-		if !bytes.Equal(test.expectedQUICTransportParams, connState.QUICTransportParams) {
+	if len(expectations.quicTransportParams) > 0 {
+		if !bytes.Equal(expectations.quicTransportParams, connState.QUICTransportParams) {
 			return errors.New("Peer did not send expected QUIC transport params")
 		}
 	}
@@ -1228,7 +1229,7 @@
 			continue
 		}
 
-		if test.config.MaxVersion == 0 && test.config.MinVersion == 0 && test.expectedVersion == 0 {
+		if test.config.MaxVersion == 0 && test.config.MinVersion == 0 && test.expectations.version == 0 {
 			panic(fmt.Sprintf("The name of test %q suggests that it's version specific, but min/max version in the Config is %x/%x. One of them should probably be %x", test.name, test.config.MinVersion, test.config.MaxVersion, ver.version))
 		}
 	}
@@ -1274,7 +1275,10 @@
 			if test.resumeConfig != nil {
 				test.resumeConfig.QUICTransportParams = []byte{1, 2}
 			}
-			test.expectedQUICTransportParams = []byte{3, 4}
+			test.expectations.quicTransportParams = []byte{3, 4}
+			if test.resumeExpectations != nil {
+				test.resumeExpectations.quicTransportParams = []byte{3, 4}
+			}
 			flags = append(flags,
 				[]string{
 					"-quic-transport-params",
@@ -1294,8 +1298,12 @@
 			if test.resumeConfig != nil {
 				test.resumeConfig.NextProtos = []string{"foo"}
 			}
-			test.expectedNextProto = "foo"
-			test.expectedNextProtoType = alpn
+			test.expectations.nextProto = "foo"
+			test.expectations.nextProtoType = alpn
+			if test.resumeExpectations != nil {
+				test.resumeExpectations.nextProto = "foo"
+				test.resumeExpectations.nextProtoType = alpn
+			}
 		}
 	}
 
@@ -1882,7 +1890,9 @@
 					SkipCertificateVerify: true,
 				},
 			},
-			expectPeerCertificate: &rsaChainCertificate,
+			expectations: connectionExpectations{
+				peerCertificate: &rsaChainCertificate,
+			},
 			flags: []string{
 				"-require-any-client-certificate",
 			},
@@ -2017,7 +2027,9 @@
 					FragmentClientVersion:    true,
 				},
 			},
-			expectedVersion: VersionTLS13,
+			expectations: connectionExpectations{
+				version: VersionTLS13,
+			},
 		},
 		{
 			testType:      serverTest,
@@ -3880,8 +3892,10 @@
 				MaxVersion:   VersionTLS12,
 				CipherSuites: t.ciphers,
 			},
-			flags:          []string{"-cipher", negotiationTestCiphers},
-			expectedCipher: t.expected,
+			flags: []string{"-cipher", negotiationTestCiphers},
+			expectations: connectionExpectations{
+				cipher: t.expected,
+			},
 		})
 	}
 }
@@ -4174,7 +4188,9 @@
 				MaxVersion: ver.version,
 				ChannelID:  channelIDKey,
 			},
-			expectChannelID: true,
+			expectations: connectionExpectations{
+				channelID: true,
+			},
 			flags: []string{
 				"-enable-channel-id",
 				"-verify-peer-if-no-obc",
@@ -5044,7 +5060,9 @@
 			config: Config{
 				MaxVersion: vers.version,
 			},
-			expectedOCSPResponse: testOCSPResponse,
+			expectations: connectionExpectations{
+				ocspResponse: testOCSPResponse,
+			},
 			flags: []string{
 				"-ocsp-response",
 				base64.StdEncoding.EncodeToString(testOCSPResponse),
@@ -5117,7 +5135,9 @@
 			config: Config{
 				MaxVersion: vers.version,
 			},
-			expectedOCSPResponse: testOCSPResponse,
+			expectations: connectionExpectations{
+				ocspResponse: testOCSPResponse,
+			},
 			flags: []string{
 				"-use-ocsp-callback",
 				"-set-ocsp-in-callback",
@@ -5136,7 +5156,9 @@
 			config: Config{
 				MaxVersion: vers.version,
 			},
-			expectedOCSPResponse: []byte{},
+			expectations: connectionExpectations{
+				ocspResponse: []byte{},
+			},
 			flags: []string{
 				"-use-ocsp-callback",
 				"-decline-ocsp-callback",
@@ -5497,10 +5519,12 @@
 				MaxVersion: VersionTLS12,
 				NextProtos: []string{"foo"},
 			},
-			flags:                 []string{"-select-next-proto", "foo"},
-			resumeSession:         true,
-			expectedNextProto:     "foo",
-			expectedNextProtoType: npn,
+			flags:         []string{"-select-next-proto", "foo"},
+			resumeSession: true,
+			expectations: connectionExpectations{
+				nextProto:     "foo",
+				nextProtoType: npn,
+			},
 		})
 		tests = append(tests, testCase{
 			testType: serverTest,
@@ -5513,9 +5537,11 @@
 				"-advertise-npn", "\x03foo\x03bar\x03baz",
 				"-expect-next-proto", "bar",
 			},
-			resumeSession:         true,
-			expectedNextProto:     "bar",
-			expectedNextProtoType: npn,
+			resumeSession: true,
+			expectations: connectionExpectations{
+				nextProto:     "bar",
+				nextProtoType: npn,
+			},
 		})
 
 		// Client does False Start and negotiates NPN.
@@ -5619,10 +5645,12 @@
 				"-send-channel-id", path.Join(*resourceDir, channelIDKeyFile),
 				"-select-next-proto", "foo",
 			},
-			resumeSession:         true,
-			expectChannelID:       true,
-			expectedNextProto:     "foo",
-			expectedNextProtoType: npn,
+			resumeSession: true,
+			expectations: connectionExpectations{
+				channelID:     true,
+				nextProto:     "foo",
+				nextProtoType: npn,
+			},
 		})
 		tests = append(tests, testCase{
 			testType: serverTest,
@@ -5638,10 +5666,12 @@
 				"-advertise-npn", "\x03foo\x03bar\x03baz",
 				"-expect-next-proto", "bar",
 			},
-			resumeSession:         true,
-			expectChannelID:       true,
-			expectedNextProto:     "bar",
-			expectedNextProtoType: npn,
+			resumeSession: true,
+			expectations: connectionExpectations{
+				channelID:     true,
+				nextProto:     "bar",
+				nextProtoType: npn,
+			},
 		})
 
 		// Bidirectional shutdown with the runner initiating.
@@ -5668,9 +5698,11 @@
 					MaxVersion:       ver.version,
 					RequestChannelID: true,
 				},
-				flags:           []string{"-send-channel-id", path.Join(*resourceDir, channelIDKeyFile)},
-				resumeSession:   true,
-				expectChannelID: true,
+				flags:         []string{"-send-channel-id", path.Join(*resourceDir, channelIDKeyFile)},
+				resumeSession: true,
+				expectations: connectionExpectations{
+					channelID: true,
+				},
 			})
 
 			// Server accepts a Channel ID.
@@ -5685,8 +5717,10 @@
 					"-expect-channel-id",
 					base64.StdEncoding.EncodeToString(channelIDBytes),
 				},
-				resumeSession:   true,
-				expectChannelID: true,
+				resumeSession: true,
+				expectations: connectionExpectations{
+					channelID: true,
+				},
 			})
 
 			tests = append(tests, testCase{
@@ -5714,11 +5748,13 @@
 						CipherSuites: []uint16{TLS_RSA_WITH_AES_128_CBC_SHA},
 						ChannelID:    channelIDKey,
 					},
-					expectChannelID: false,
-					flags:           []string{"-enable-channel-id"},
+					expectations: connectionExpectations{
+						channelID: false,
+					},
+					flags: []string{"-enable-channel-id"},
 				})
 
-				// Sanity-check setting expectChannelID false works.
+				// Sanity-check setting expectations.channelID false works.
 				tests = append(tests, testCase{
 					testType: serverTest,
 					name:     "ChannelID-ECDHE-" + ver.name,
@@ -5727,7 +5763,9 @@
 						CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA},
 						ChannelID:    channelIDKey,
 					},
-					expectChannelID:    false,
+					expectations: connectionExpectations{
+						channelID: false,
+					},
 					flags:              []string{"-enable-channel-id"},
 					shouldFail:         true,
 					expectedLocalError: "channel ID unexpectedly negotiated",
@@ -6018,8 +6056,10 @@
 							ExpectInitialRecordVersion: clientVers,
 						},
 					},
-					flags:           flags,
-					expectedVersion: expectedVersion,
+					flags: flags,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 				})
 				testCases = append(testCases, testCase{
 					protocol: protocol,
@@ -6031,8 +6071,10 @@
 							ExpectInitialRecordVersion: clientVers,
 						},
 					},
-					flags:           flags2,
-					expectedVersion: expectedVersion,
+					flags: flags2,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 				})
 
 				testCases = append(testCases, testCase{
@@ -6045,8 +6087,10 @@
 							ExpectInitialRecordVersion: serverVers,
 						},
 					},
-					flags:           flags,
-					expectedVersion: expectedVersion,
+					flags: flags,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 				})
 				testCases = append(testCases, testCase{
 					protocol: protocol,
@@ -6058,8 +6102,10 @@
 							ExpectInitialRecordVersion: serverVers,
 						},
 					},
-					flags:           flags2,
-					expectedVersion: expectedVersion,
+					flags: flags2,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 				})
 			}
 		}
@@ -6080,7 +6126,9 @@
 						IgnoreTLS13DowngradeRandom: true,
 					},
 				},
-				expectedVersion: vers.version,
+				expectations: connectionExpectations{
+					version: vers.version,
+				},
 			})
 		}
 	}
@@ -6121,7 +6169,9 @@
 				IgnoreTLS13DowngradeRandom: true,
 			},
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -6135,7 +6185,9 @@
 			},
 		},
 		// The extension takes precedence over the ClientHello version.
-		expectedVersion: VersionTLS11,
+		expectations: connectionExpectations{
+			version: VersionTLS11,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -6149,7 +6201,9 @@
 			},
 		},
 		// The extension takes precedence over the ClientHello version.
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 
 	// Test that TLS 1.2 isn't negotiated by the supported_versions extension in
@@ -6177,7 +6231,9 @@
 				SendSupportedVersions: []uint16{VersionTLS12, VersionTLS13},
 			},
 		},
-		expectedVersion: VersionTLS13,
+		expectations: connectionExpectations{
+			version: VersionTLS13,
+		},
 	})
 
 	// Test for version tolerance.
@@ -6191,7 +6247,9 @@
 				IgnoreTLS13DowngradeRandom: true,
 			},
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 	testCases = append(testCases, testCase{
 		testType: serverTest,
@@ -6205,7 +6263,9 @@
 		},
 		// TLS 1.3 must be negotiated with the supported_versions
 		// extension, not ClientHello.version.
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 	testCases = append(testCases, testCase{
 		testType: serverTest,
@@ -6218,7 +6278,9 @@
 				SendClientVersion: 0x0400,
 			},
 		},
-		expectedVersion: VersionTLS13,
+		expectations: connectionExpectations{
+			version: VersionTLS13,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -6231,7 +6293,9 @@
 				OmitSupportedVersions: true,
 			},
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 	testCases = append(testCases, testCase{
 		protocol: dtls,
@@ -6243,7 +6307,9 @@
 				OmitSupportedVersions: true,
 			},
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 	})
 
 	// Test that versions below 3.0 are rejected.
@@ -6304,7 +6370,9 @@
 					NegotiateVersion: test.version,
 				},
 			},
-			expectedVersion:    test.version,
+			expectations: connectionExpectations{
+				version: test.version,
+			},
 			shouldFail:         true,
 			expectedError:      ":TLS13_DOWNGRADE:",
 			expectedLocalError: "remote error: illegal parameter",
@@ -6319,7 +6387,9 @@
 					NegotiateVersion: test.version,
 				},
 			},
-			expectedVersion: test.version,
+			expectations: connectionExpectations{
+				version: test.version,
+			},
 			flags: []string{
 				"-ignore-tls13-downgrade",
 				"-expect-tls13-downgrade",
@@ -6335,7 +6405,9 @@
 					SendSupportedVersions: []uint16{test.version},
 				},
 			},
-			expectedVersion:    test.version,
+			expectations: connectionExpectations{
+				version: test.version,
+			},
 			shouldFail:         true,
 			expectedLocalError: test.clientShimError,
 		})
@@ -6352,7 +6424,9 @@
 				AlertBeforeFalseStartTest: alertAccessDenied,
 			},
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 		flags: []string{
 			"-false-start",
 			"-advertise-alpn", "\x03foo",
@@ -6447,8 +6521,10 @@
 							IgnorePeerCipherPreferences: shouldFail,
 						},
 					},
-					flags:              flags,
-					expectedVersion:    expectedVersion,
+					flags: flags,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 					shouldFail:         shouldFail,
 					expectedError:      expectedError,
 					expectedLocalError: expectedLocalError,
@@ -6467,8 +6543,10 @@
 							IgnorePeerCipherPreferences: shouldFail,
 						},
 					},
-					flags:              flags2,
-					expectedVersion:    expectedVersion,
+					flags: flags2,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 					shouldFail:         shouldFail,
 					expectedError:      expectedError,
 					expectedLocalError: expectedLocalError,
@@ -6481,8 +6559,10 @@
 					config: Config{
 						MaxVersion: runnerVers.version,
 					},
-					flags:              flags,
-					expectedVersion:    expectedVersion,
+					flags: flags,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 					shouldFail:         shouldFail,
 					expectedError:      expectedError,
 					expectedLocalError: expectedLocalError,
@@ -6494,8 +6574,10 @@
 					config: Config{
 						MaxVersion: runnerVers.version,
 					},
-					flags:              flags2,
-					expectedVersion:    expectedVersion,
+					flags: flags2,
+					expectations: connectionExpectations{
+						version: expectedVersion,
+					},
 					shouldFail:         shouldFail,
 					expectedError:      expectedError,
 					expectedLocalError: expectedLocalError,
@@ -6623,9 +6705,11 @@
 				"-advertise-alpn", "\x03foo\x03bar\x03baz",
 				"-expect-alpn", "foo",
 			},
-			expectedNextProto:     "foo",
-			expectedNextProtoType: alpn,
-			resumeSession:         true,
+			expectations: connectionExpectations{
+				nextProto:     "foo",
+				nextProtoType: alpn,
+			},
+			resumeSession: true,
 		})
 		testCases = append(testCases, testCase{
 			testType: clientTest,
@@ -6669,9 +6753,11 @@
 				"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
 				"-select-alpn", "foo",
 			},
-			expectedNextProto:     "foo",
-			expectedNextProtoType: alpn,
-			resumeSession:         true,
+			expectations: connectionExpectations{
+				nextProto:     "foo",
+				nextProtoType: alpn,
+			},
+			resumeSession: true,
 		})
 		testCases = append(testCases, testCase{
 			testType: serverTest,
@@ -6680,9 +6766,11 @@
 				MaxVersion: ver.version,
 				NextProtos: []string{"foo", "bar", "baz"},
 			},
-			flags:             []string{"-decline-alpn"},
-			expectNoNextProto: true,
-			resumeSession:     true,
+			flags: []string{"-decline-alpn"},
+			expectations: connectionExpectations{
+				noNextProto: true,
+			},
+			resumeSession: true,
 		})
 		// Test that the server implementation catches itself if the
 		// callback tries to return an invalid empty ALPN protocol.
@@ -6718,9 +6806,11 @@
 				"-select-alpn", "foo",
 				"-async",
 			},
-			expectedNextProto:     "foo",
-			expectedNextProtoType: alpn,
-			resumeSession:         true,
+			expectations: connectionExpectations{
+				nextProto:     "foo",
+				nextProtoType: alpn,
+			},
+			resumeSession: true,
 		})
 
 		var emptyString string
@@ -6773,9 +6863,11 @@
 					"-select-alpn", "foo",
 					"-advertise-npn", "\x03foo\x03bar\x03baz",
 				},
-				expectedNextProto:     "foo",
-				expectedNextProtoType: alpn,
-				resumeSession:         true,
+				expectations: connectionExpectations{
+					nextProto:     "foo",
+					nextProtoType: alpn,
+				},
+				resumeSession: true,
 			})
 			testCases = append(testCases, testCase{
 				testType: serverTest,
@@ -6792,9 +6884,11 @@
 					"-select-alpn", "foo",
 					"-advertise-npn", "\x03foo\x03bar\x03baz",
 				},
-				expectedNextProto:     "foo",
-				expectedNextProtoType: alpn,
-				resumeSession:         true,
+				expectations: connectionExpectations{
+					nextProto:     "foo",
+					nextProtoType: alpn,
+				},
+				resumeSession: true,
 			})
 
 			// Test that negotiating both NPN and ALPN is forbidden.
@@ -6909,8 +7003,10 @@
 				TokenBindingParams:  []byte{0, 1, 2},
 				TokenBindingVersion: maxTokenBindingVersion,
 			},
-			expectTokenBinding:        true,
-			expectedTokenBindingParam: 2,
+			expectations: connectionExpectations{
+				tokenBinding:      true,
+				tokenBindingParam: 2,
+			},
 			flags: []string{
 				"-token-binding-params",
 				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
@@ -6958,8 +7054,10 @@
 				TokenBindingParams:  []byte{0, 1, 2},
 				TokenBindingVersion: maxTokenBindingVersion + 1,
 			},
-			expectTokenBinding:        true,
-			expectedTokenBindingParam: 2,
+			expectations: connectionExpectations{
+				tokenBinding:      true,
+				tokenBindingParam: 2,
+			},
 			flags: []string{
 				"-token-binding-params",
 				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
@@ -6994,8 +7092,10 @@
 				TokenBindingParams:  []byte{0, 1, 2, 2},
 				TokenBindingVersion: maxTokenBindingVersion,
 			},
-			expectTokenBinding:        true,
-			expectedTokenBindingParam: 2,
+			expectations: connectionExpectations{
+				tokenBinding:      true,
+				tokenBindingParam: 2,
+			},
 			flags: []string{
 				"-token-binding-params",
 				base64.StdEncoding.EncodeToString([]byte{2, 1, 0}),
@@ -7261,9 +7361,11 @@
 					TokenBindingVersion: maxTokenBindingVersion,
 					MaxEarlyDataSize:    16384,
 				},
-				resumeSession:             true,
-				expectTokenBinding:        true,
-				expectedTokenBindingParam: 2,
+				resumeSession: true,
+				expectations: connectionExpectations{
+					tokenBinding:      true,
+					tokenBindingParam: 2,
+				},
 				flags: []string{
 					"-enable-early-data",
 					"-expect-ticket-supports-early-data",
@@ -7293,8 +7395,10 @@
 					"-expect-quic-transport-params",
 					base64.StdEncoding.EncodeToString([]byte{1, 2}),
 				},
-				expectedQUICTransportParams: []byte{3, 4},
-				skipTransportParamsConfig:   true,
+				expectations: connectionExpectations{
+					quicTransportParams: []byte{3, 4},
+				},
+				skipTransportParamsConfig: true,
 			})
 			testCases = append(testCases, testCase{
 				testType: clientTest,
@@ -7328,8 +7432,10 @@
 					"-expect-quic-transport-params",
 					base64.StdEncoding.EncodeToString([]byte{1, 2}),
 				},
-				expectedQUICTransportParams: []byte{3, 4},
-				skipTransportParamsConfig:   true,
+				expectations: connectionExpectations{
+					quicTransportParams: []byte{3, 4},
+				},
+				skipTransportParamsConfig: true,
 			})
 			testCases = append(testCases, testCase{
 				testType: serverTest,
@@ -7343,10 +7449,12 @@
 					"-quic-transport-params",
 					base64.StdEncoding.EncodeToString([]byte{3, 4}),
 				},
-				expectedQUICTransportParams: []byte{3, 4},
-				shouldFail:                  true,
-				expectedError:               ":MISSING_EXTENSION:",
-				skipTransportParamsConfig:   true,
+				expectations: connectionExpectations{
+					quicTransportParams: []byte{3, 4},
+				},
+				shouldFail:                true,
+				expectedError:             ":MISSING_EXTENSION:",
+				skipTransportParamsConfig: true,
 			})
 		}
 		testCases = append(testCases, testCase{
@@ -7512,7 +7620,9 @@
 					"-srtp-profiles",
 					"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
 				},
-				expectedSRTPProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				expectations: connectionExpectations{
+					srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				},
 			})
 			testCases = append(testCases, testCase{
 				protocol: dtls,
@@ -7526,7 +7636,9 @@
 					"-srtp-profiles",
 					"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
 				},
-				expectedSRTPProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				expectations: connectionExpectations{
+					srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				},
 			})
 			// Test that the MKI is ignored.
 			testCases = append(testCases, testCase{
@@ -7544,7 +7656,9 @@
 					"-srtp-profiles",
 					"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
 				},
-				expectedSRTPProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				expectations: connectionExpectations{
+					srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
+				},
 			})
 			// Test that SRTP isn't negotiated on the server if there were
 			// no matching profiles.
@@ -7560,7 +7674,9 @@
 					"-srtp-profiles",
 					"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
 				},
-				expectedSRTPProtectionProfile: 0,
+				expectations: connectionExpectations{
+					srtpProtectionProfile: 0,
+				},
 			})
 			// Test that the server returning an invalid SRTP profile is
 			// flagged as an error by the client.
@@ -7629,8 +7745,10 @@
 				"-signed-cert-timestamps",
 				base64.StdEncoding.EncodeToString(testSCTList),
 			},
-			expectedSCTList: testSCTList,
-			resumeSession:   true,
+			expectations: connectionExpectations{
+				sctList: testSCTList,
+			},
+			resumeSession: true,
 		})
 
 		emptySCTListCert := *testCerts[0].cert
@@ -7960,8 +8078,12 @@
 								ExpectNoTLS13PSK: sessionVers.version < VersionTLS13,
 							},
 						},
-						expectedVersion:       sessionVers.version,
-						expectedResumeVersion: resumeVers.version,
+						expectations: connectionExpectations{
+							version: sessionVers.version,
+						},
+						resumeExpectations: &connectionExpectations{
+							version: resumeVers.version,
+						},
 					})
 				} else {
 					testCases = append(testCases, testCase{
@@ -7971,16 +8093,20 @@
 						config: Config{
 							MaxVersion: sessionVers.version,
 						},
-						expectedVersion: sessionVers.version,
+						expectations: connectionExpectations{
+							version: sessionVers.version,
+						},
 						resumeConfig: &Config{
 							MaxVersion: resumeVers.version,
 							Bugs: ProtocolBugs{
 								AcceptAnySession: true,
 							},
 						},
-						expectedResumeVersion: resumeVers.version,
-						shouldFail:            true,
-						expectedError:         ":OLD_SESSION_VERSION_NOT_RETURNED:",
+						resumeExpectations: &connectionExpectations{
+							version: resumeVers.version,
+						},
+						shouldFail:    true,
+						expectedError: ":OLD_SESSION_VERSION_NOT_RETURNED:",
 					})
 				}
 
@@ -7991,13 +8117,17 @@
 					config: Config{
 						MaxVersion: sessionVers.version,
 					},
-					expectedVersion: sessionVers.version,
+					expectations: connectionExpectations{
+						version: sessionVers.version,
+					},
 					resumeConfig: &Config{
 						MaxVersion: resumeVers.version,
 					},
-					newSessionsOnResume:   true,
-					expectResumeRejected:  true,
-					expectedResumeVersion: resumeVers.version,
+					newSessionsOnResume:  true,
+					expectResumeRejected: true,
+					resumeExpectations: &connectionExpectations{
+						version: resumeVers.version,
+					},
 				})
 
 				testCases = append(testCases, testCase{
@@ -8008,7 +8138,9 @@
 					config: Config{
 						MaxVersion: sessionVers.version,
 					},
-					expectedVersion:      sessionVers.version,
+					expectations: connectionExpectations{
+						version: sessionVers.version,
+					},
 					expectResumeRejected: sessionVers != resumeVers,
 					resumeConfig: &Config{
 						MaxVersion: resumeVers.version,
@@ -8016,7 +8148,9 @@
 							SendBothTickets: true,
 						},
 					},
-					expectedResumeVersion: resumeVers.version,
+					resumeExpectations: &connectionExpectations{
+						version: resumeVers.version,
+					},
 				})
 
 				// Repeat the test using session IDs, rather than tickets.
@@ -8030,13 +8164,17 @@
 							MaxVersion:             sessionVers.version,
 							SessionTicketsDisabled: true,
 						},
-						expectedVersion:      sessionVers.version,
+						expectations: connectionExpectations{
+							version: sessionVers.version,
+						},
 						expectResumeRejected: sessionVers != resumeVers,
 						resumeConfig: &Config{
 							MaxVersion:             resumeVers.version,
 							SessionTicketsDisabled: true,
 						},
-						expectedResumeVersion: resumeVers.version,
+						resumeExpectations: &connectionExpectations{
+							version: resumeVers.version,
+						},
 					})
 				}
 			}
@@ -9125,10 +9263,12 @@
 						"-key-file", path.Join(*resourceDir, getShimKey(alg.cert)),
 						"-enable-all-curves",
 					},
-					shouldFail:                     shouldFail,
-					expectedError:                  signError,
-					expectedLocalError:             signLocalError,
-					expectedPeerSignatureAlgorithm: alg.id,
+					shouldFail:         shouldFail,
+					expectedError:      signError,
+					expectedLocalError: signLocalError,
+					expectations: connectionExpectations{
+						peerSignatureAlgorithm: alg.id,
+					},
 				}
 
 				// Test that the shim will select the algorithm when configured to only
@@ -9146,7 +9286,9 @@
 						"-enable-all-curves",
 						"-signing-prefs", strconv.Itoa(int(alg.id)),
 					},
-					expectedPeerSignatureAlgorithm: alg.id,
+					expectations: connectionExpectations{
+						peerSignatureAlgorithm: alg.id,
+					},
 				}
 
 				if testType == serverTest {
@@ -9328,7 +9470,9 @@
 			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
 			"-key-file", path.Join(*resourceDir, rsaKeyFile),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA384,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA384,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -9347,7 +9491,9 @@
 			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
 			"-key-file", path.Join(*resourceDir, rsaKeyFile),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPSSWithSHA384,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPSSWithSHA384,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -9362,7 +9508,9 @@
 				signatureECDSAWithSHA1,
 			},
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA384,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA384,
+		},
 	})
 
 	testCases = append(testCases, testCase{
@@ -9377,7 +9525,9 @@
 				signatureECDSAWithSHA1,
 			},
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPSSWithSHA384,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPSSWithSHA384,
+		},
 	})
 
 	// Test that signature verification takes the key type into account.
@@ -9696,7 +9846,9 @@
 			"-signing-prefs", strconv.Itoa(int(signatureRSAPKCS1WithSHA256)),
 			"-signing-prefs", strconv.Itoa(int(signatureRSAPKCS1WithSHA1)),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		},
 	})
 	testCases = append(testCases, testCase{
 		name: "Agree-Digest-SHA1",
@@ -9714,7 +9866,9 @@
 			"-signing-prefs", strconv.Itoa(int(signatureRSAPKCS1WithSHA256)),
 			"-signing-prefs", strconv.Itoa(int(signatureRSAPKCS1WithSHA1)),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA1,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA1,
+		},
 	})
 	testCases = append(testCases, testCase{
 		name: "Agree-Digest-Default",
@@ -9732,7 +9886,9 @@
 			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
 			"-key-file", path.Join(*resourceDir, rsaKeyFile),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		},
 	})
 
 	// Test that the signing preference list may include extra algorithms
@@ -9754,7 +9910,9 @@
 			"-signing-prefs", strconv.Itoa(int(signatureRSAPKCS1WithSHA256)),
 			"-signing-prefs", strconv.Itoa(int(fakeSigAlg2)),
 		},
-		expectedPeerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureRSAPKCS1WithSHA256,
+		},
 	})
 
 	// In TLS 1.2 and below, ECDSA uses the curve list rather than the
@@ -9827,7 +9985,9 @@
 			"-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile),
 			"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
 		},
-		expectedPeerSignatureAlgorithm: signatureECDSAWithP256AndSHA256,
+		expectations: connectionExpectations{
+			peerSignatureAlgorithm: signatureECDSAWithP256AndSHA256,
+		},
 	})
 
 	// RSASSA-PSS with SHA-512 is too large for 1024-bit RSA. Test that the
@@ -10611,7 +10771,9 @@
 					"-enable-all-curves",
 					"-expect-curve-id", strconv.Itoa(int(curve.id)),
 				},
-				expectedCurveID: curve.id,
+				expectations: connectionExpectations{
+					curveID: curve.id,
+				},
 			})
 			testCases = append(testCases, testCase{
 				testType: serverTest,
@@ -10629,7 +10791,9 @@
 					"-enable-all-curves",
 					"-expect-curve-id", strconv.Itoa(int(curve.id)),
 				},
-				expectedCurveID: curve.id,
+				expectations: connectionExpectations{
+					curveID: curve.id,
+				},
 			})
 
 			if curve.id != CurveX25519 && !isPqGroup(curve.id) {
@@ -10736,7 +10900,9 @@
 			},
 			CurvePreferences: []CurveID{CurveP224},
 		},
-		expectedCipher: TLS_RSA_WITH_AES_128_GCM_SHA256,
+		expectations: connectionExpectations{
+			cipher: TLS_RSA_WITH_AES_128_GCM_SHA256,
+		},
 	})
 
 	// The client must reject bogus curves and disabled curves.
@@ -11226,8 +11392,10 @@
 				SendPSKKeyExchangeModes: []byte{0x1a, pskDHEKEMode, 0x2a},
 			},
 		},
-		resumeSession:         true,
-		expectedResumeVersion: VersionTLS13,
+		resumeSession: true,
+		expectations: connectionExpectations{
+			version: VersionTLS13,
+		},
 	})
 
 	// Test that the server does not send session tickets with no matching key exchange mode.
@@ -12864,8 +13032,10 @@
 			DefaultCurves:    []CurveID{},
 			CurvePreferences: []CurveID{CurveX25519},
 		},
-		expectedCurveID: CurveX25519,
-		flags:           []string{"-expect-hrr"},
+		expectations: connectionExpectations{
+			curveID: CurveX25519,
+		},
+		flags: []string{"-expect-hrr"},
 	})
 
 	testCases = append(testCases, testCase{
@@ -12878,8 +13048,10 @@
 		},
 		// Although the ClientHello did not predict our preferred curve,
 		// we always select it whether it is predicted or not.
-		expectedCurveID: CurveX25519,
-		flags:           []string{"-expect-hrr"},
+		expectations: connectionExpectations{
+			curveID: CurveX25519,
+		},
+		flags: []string{"-expect-hrr"},
 	})
 
 	testCases = append(testCases, testCase{
@@ -13883,8 +14055,10 @@
 			MaxEarlyDataSize: 16384,
 			RequestChannelID: true,
 		},
-		resumeSession:      true,
-		expectChannelID:    true,
+		resumeSession: true,
+		expectations: connectionExpectations{
+			channelID: true,
+		},
 		shouldFail:         true,
 		expectedError:      ":UNEXPECTED_EXTENSION_ON_EARLY_DATA:",
 		expectedLocalError: "remote error: illegal parameter",
@@ -13908,8 +14082,10 @@
 				AlwaysRejectEarlyData: true,
 			},
 		},
-		resumeSession:   true,
-		expectChannelID: true,
+		resumeSession: true,
+		expectations: connectionExpectations{
+			channelID: true,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-expect-ticket-supports-early-data",
@@ -13952,8 +14128,10 @@
 				ExpectEarlyDataAccepted: false,
 			},
 		},
-		resumeSession:   true,
-		expectChannelID: true,
+		resumeSession: true,
+		expectations: connectionExpectations{
+			channelID: true,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-expect-reject-early-data",
@@ -13976,8 +14154,10 @@
 				ExpectHalfRTTData:       [][]byte{{254, 253, 252, 251}},
 			},
 		},
-		resumeSession:   true,
-		expectChannelID: false,
+		resumeSession: true,
+		expectations: connectionExpectations{
+			channelID: false,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-on-resume-expect-accept-early-data",
@@ -14133,7 +14313,9 @@
 				SkipCertificateVerify: true,
 			},
 		},
-		expectPeerCertificate: &rsaChainCertificate,
+		expectations: connectionExpectations{
+			peerCertificate: &rsaChainCertificate,
+		},
 		flags: []string{
 			"-cert-file", path.Join(*resourceDir, rsaChainCertificateFile),
 			"-key-file", path.Join(*resourceDir, rsaChainKeyFile),
@@ -14154,7 +14336,9 @@
 				SkipCertificateVerify: true,
 			},
 		},
-		expectPeerCertificate: &rsaChainCertificate,
+		expectations: connectionExpectations{
+			peerCertificate: &rsaChainCertificate,
+		},
 		flags: []string{
 			"-cert-file", path.Join(*resourceDir, rsaChainCertificateFile),
 			"-key-file", path.Join(*resourceDir, rsaChainKeyFile),
@@ -14167,9 +14351,11 @@
 	// If the client or server has 0-RTT enabled but disabled TLS 1.3, it should
 	// report a reason of protocol_version.
 	testCases = append(testCases, testCase{
-		testType:        clientTest,
-		name:            "EarlyDataEnabled-Client-MaxTLS12",
-		expectedVersion: VersionTLS12,
+		testType: clientTest,
+		name:     "EarlyDataEnabled-Client-MaxTLS12",
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-max-version", strconv.Itoa(VersionTLS12),
@@ -14177,9 +14363,11 @@
 		},
 	})
 	testCases = append(testCases, testCase{
-		testType:        serverTest,
-		name:            "EarlyDataEnabled-Server-MaxTLS12",
-		expectedVersion: VersionTLS12,
+		testType: serverTest,
+		name:     "EarlyDataEnabled-Server-MaxTLS12",
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-max-version", strconv.Itoa(VersionTLS12),
@@ -14197,7 +14385,9 @@
 		config: Config{
 			MaxVersion: VersionTLS12,
 		},
-		expectedVersion: VersionTLS12,
+		expectations: connectionExpectations{
+			version: VersionTLS12,
+		},
 		flags: []string{
 			"-enable-early-data",
 			"-expect-early-data-reason", "protocol_version",
@@ -14368,7 +14558,9 @@
 		flags: []string{
 			"-curves", strconv.Itoa(int(CurveCECPQ2)),
 		},
-		expectedCipher: TLS_AES_256_GCM_SHA384,
+		expectations: connectionExpectations{
+			cipher: TLS_AES_256_GCM_SHA384,
+		},
 	})
 	// ... but when CECPQ2 isn't being used, the client's preference controls.
 	testCases = append(testCases, testCase{
@@ -14384,7 +14576,9 @@
 		flags: []string{
 			"-curves", strconv.Itoa(int(CurveX25519)),
 		},
-		expectedCipher: TLS_AES_128_GCM_SHA256,
+		expectations: connectionExpectations{
+			cipher: TLS_AES_128_GCM_SHA256,
+		},
 	})
 
 	// Test that CECPQ2 continues to honor AES vs ChaCha20 logic.
@@ -14554,7 +14748,9 @@
 				Certificates: []Certificate{rsaChainCertificate},
 				ClientAuth:   RequireAnyClientCert,
 			},
-			expectPeerCertificate: &rsaChainCertificate,
+			expectations: connectionExpectations{
+				peerCertificate: &rsaChainCertificate,
+			},
 			flags: []string{
 				"-cert-file", path.Join(*resourceDir, rsaChainCertificateFile),
 				"-key-file", path.Join(*resourceDir, rsaChainKeyFile),
@@ -14570,7 +14766,9 @@
 				MaxVersion:   ver.version,
 				Certificates: []Certificate{rsaChainCertificate},
 			},
-			expectPeerCertificate: &rsaChainCertificate,
+			expectations: connectionExpectations{
+				peerCertificate: &rsaChainCertificate,
+			},
 			flags: []string{
 				"-cert-file", path.Join(*resourceDir, rsaChainCertificateFile),
 				"-key-file", path.Join(*resourceDir, rsaChainKeyFile),
@@ -15502,8 +15700,10 @@
 					ExpectJDK11DowngradeRandom: t.isJDK11,
 				},
 			},
-			expectedVersion: expectedVersion,
-			flags:           []string{"-jdk11-workaround"},
+			expectations: connectionExpectations{
+				version: expectedVersion,
+			},
+			flags: []string{"-jdk11-workaround"},
 		})
 
 		// With the workaround disabled, we always negotiate TLS 1.3.
@@ -15518,7 +15718,9 @@
 					ExpectJDK11DowngradeRandom: false,
 				},
 			},
-			expectedVersion: VersionTLS13,
+			expectations: connectionExpectations{
+				version: VersionTLS13,
+			},
 		})
 
 		// If the server does not support TLS 1.3, the workaround should
@@ -15535,7 +15737,9 @@
 					ExpectJDK11DowngradeRandom: false,
 				},
 			},
-			expectedVersion: VersionTLS12,
+			expectations: connectionExpectations{
+				version: VersionTLS12,
+			},
 			flags: []string{
 				"-jdk11-workaround",
 				"-max-version", strconv.Itoa(VersionTLS12),