snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
27
internal/antithesis/noop.go
Normal file
27
internal/antithesis/noop.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2022-2024 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file is used iff the `enable_antithesis_sdk` build tag is not present
|
||||
//go:build !enable_antithesis_sdk
|
||||
|
||||
package antithesis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// AssertUnreachable this implementation is a NOOP
|
||||
func AssertUnreachable(_ testing.TB, _ string, _ map[string]any) {}
|
||||
|
||||
// Assert this implementation is a NOOP
|
||||
func Assert(_ testing.TB, _ bool, _ string, _ map[string]any) {}
|
||||
111
internal/antithesis/test_assert.go
Normal file
111
internal/antithesis/test_assert.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2022-2024 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file is used iff the `enable_antithesis_sdk` build tag is present
|
||||
//go:build enable_antithesis_sdk
|
||||
|
||||
package antithesis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
|
||||
"github.com/antithesishq/antithesis-sdk-go/assert"
|
||||
)
|
||||
|
||||
// This file provides assertions utility functions suitable for use in tests.
|
||||
// It is a thin wrapper around the Antithesis SDK.
|
||||
//
|
||||
// Notice that unlike other assertions libraries, a violation does not halt execution or fail the test.
|
||||
// The effects of violating an assertion, are:
|
||||
// 1. Print the violation message, prefixed by the test name in which it happened
|
||||
// 2. Print the stack for the calling goroutine so each failed test has a clear trace of the failure
|
||||
// 3. Invoke the underlying Antithesis assertion
|
||||
//
|
||||
// N.B. Enabling this module outside for tests running outside of Antithesis will enable 1 and 2 above, but not 3.
|
||||
// therefore it can be useful to output additional test failure details when running tests locally or in CI.
|
||||
|
||||
// AssertUnreachable is used to flag code branches that should not get invoked ever.
|
||||
// Example:
|
||||
//
|
||||
// pubAck, err := js.Publish(...)
|
||||
//
|
||||
// if err != nil {
|
||||
// antithesis.AssertUnreachable(t, "Publish failed", map[string]any{"error": err.Error()})
|
||||
// t.Fatalf("Publish failed with error: %s", err)
|
||||
// }
|
||||
func AssertUnreachable(t testing.TB, message string, details map[string]any) {
|
||||
// Always print a message
|
||||
fmt.Printf("{*} [%s] Assert Unreachable violation: %s\n", t.Name(), message)
|
||||
if details != nil && len(details) > 0 {
|
||||
fmt.Printf("{*} Details:\n")
|
||||
jsonDetails, err := json.MarshalIndent(details, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(jsonDetails))
|
||||
}
|
||||
|
||||
// Always print the stack trace
|
||||
fmt.Printf("{*} Stack trace:\n")
|
||||
debug.PrintStack()
|
||||
|
||||
// N.B. as of today, message (as-is) is the unique identifier of the event
|
||||
// Therefore this will de-duplicate the same assertion failing in 2 different tests
|
||||
// But not the same assertion failing at 2 different lines of the same test
|
||||
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
|
||||
|
||||
// Fire assertion violation event (if Antithesis is enabled)
|
||||
assert.Unreachable(messageWithTestName, details)
|
||||
}
|
||||
|
||||
// Assert is used to check that some given condition is always true,
|
||||
// Example:
|
||||
//
|
||||
// antithesis.Assert(t, sequence > lastSequence, "Non-monotonic stream sequence number", map[string]any{
|
||||
// "stream": streamName,
|
||||
// "connection_id": nc.Id(),
|
||||
// "sequence": sequence,
|
||||
// "lastSequence": lastSequence,
|
||||
// })
|
||||
func Assert(t testing.TB, condition bool, message string, details map[string]any) {
|
||||
// Condition is true, nothing to do
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
|
||||
// Always print a message
|
||||
fmt.Printf("{*} [%s] Assert violation: %s\n", t.Name(), message)
|
||||
if details != nil && len(details) > 0 {
|
||||
fmt.Printf("{*} Details:\n")
|
||||
jsonDetails, err := json.MarshalIndent(details, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(jsonDetails))
|
||||
}
|
||||
|
||||
// Always print the stack trace
|
||||
fmt.Printf("{*} Stack trace:\n")
|
||||
debug.PrintStack()
|
||||
|
||||
// N.B. as of today, message (as-is) is the unique identifier of the event
|
||||
// Therefore this will de-duplicate the same assertion failing in 2 different tests
|
||||
// But not the same assertion failing at 2 different lines of the same test
|
||||
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
|
||||
|
||||
// Fire assertion violation event (if Antithesis is enabled)
|
||||
assert.AlwaysOrUnreachable(false, messageWithTestName, details)
|
||||
}
|
||||
305
internal/ldap/dn.go
Normal file
305
internal/ldap/dn.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
|
||||
// Portions copyright (c) 2015-2016 go-ldap Authors
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
enchex "encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var attributeTypeNames = map[string]string{
|
||||
"2.5.4.3": "CN",
|
||||
"2.5.4.5": "SERIALNUMBER",
|
||||
"2.5.4.6": "C",
|
||||
"2.5.4.7": "L",
|
||||
"2.5.4.8": "ST",
|
||||
"2.5.4.9": "STREET",
|
||||
"2.5.4.10": "O",
|
||||
"2.5.4.11": "OU",
|
||||
"2.5.4.17": "POSTALCODE",
|
||||
// FIXME: Add others.
|
||||
"0.9.2342.19200300.100.1.25": "DC",
|
||||
}
|
||||
|
||||
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
|
||||
type AttributeTypeAndValue struct {
|
||||
// Type is the attribute type
|
||||
Type string
|
||||
// Value is the attribute value
|
||||
Value string
|
||||
}
|
||||
|
||||
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
|
||||
type RelativeDN struct {
|
||||
Attributes []*AttributeTypeAndValue
|
||||
}
|
||||
|
||||
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
|
||||
type DN struct {
|
||||
RDNs []*RelativeDN
|
||||
}
|
||||
|
||||
// FromCertSubject takes a pkix.Name from a cert and returns a DN
|
||||
// that uses the same set. Does not support multi value RDNs.
|
||||
func FromCertSubject(subject pkix.Name) (*DN, error) {
|
||||
dn := &DN{
|
||||
RDNs: make([]*RelativeDN, 0),
|
||||
}
|
||||
for i := len(subject.Names) - 1; i >= 0; i-- {
|
||||
name := subject.Names[i]
|
||||
oidString := name.Type.String()
|
||||
typeName, ok := attributeTypeNames[oidString]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type name: %+v", name)
|
||||
}
|
||||
v, ok := name.Value.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type value: %+v", v)
|
||||
}
|
||||
rdn := &RelativeDN{
|
||||
Attributes: []*AttributeTypeAndValue{
|
||||
{
|
||||
Type: typeName,
|
||||
Value: v,
|
||||
},
|
||||
},
|
||||
}
|
||||
dn.RDNs = append(dn.RDNs, rdn)
|
||||
}
|
||||
return dn, nil
|
||||
}
|
||||
|
||||
// FromRawCertSubject takes a raw subject from a certificate
|
||||
// and uses asn1.Unmarshal to get the individual RDNs in the
|
||||
// original order, including multi-value RDNs.
|
||||
func FromRawCertSubject(rawSubject []byte) (*DN, error) {
|
||||
dn := &DN{
|
||||
RDNs: make([]*RelativeDN, 0),
|
||||
}
|
||||
var rdns pkix.RDNSequence
|
||||
_, err := asn1.Unmarshal(rawSubject, &rdns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := len(rdns) - 1; i >= 0; i-- {
|
||||
rdn := rdns[i]
|
||||
if len(rdn) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
r := &RelativeDN{}
|
||||
attrs := make([]*AttributeTypeAndValue, 0)
|
||||
for j := len(rdn) - 1; j >= 0; j-- {
|
||||
atv := rdn[j]
|
||||
|
||||
typeName := ""
|
||||
name := atv.Type.String()
|
||||
typeName, ok := attributeTypeNames[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type name: %+v", name)
|
||||
}
|
||||
value, ok := atv.Value.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid type value: %+v", atv.Value)
|
||||
}
|
||||
attr := &AttributeTypeAndValue{
|
||||
Type: typeName,
|
||||
Value: value,
|
||||
}
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
r.Attributes = attrs
|
||||
dn.RDNs = append(dn.RDNs, r)
|
||||
}
|
||||
|
||||
return dn, nil
|
||||
}
|
||||
|
||||
// ParseDN returns a distinguishedName or an error.
|
||||
// The function respects https://tools.ietf.org/html/rfc4514
|
||||
func ParseDN(str string) (*DN, error) {
|
||||
dn := new(DN)
|
||||
dn.RDNs = make([]*RelativeDN, 0)
|
||||
rdn := new(RelativeDN)
|
||||
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
|
||||
buffer := bytes.Buffer{}
|
||||
attribute := new(AttributeTypeAndValue)
|
||||
escaping := false
|
||||
|
||||
unescapedTrailingSpaces := 0
|
||||
stringFromBuffer := func() string {
|
||||
s := buffer.String()
|
||||
s = s[0 : len(s)-unescapedTrailingSpaces]
|
||||
buffer.Reset()
|
||||
unescapedTrailingSpaces = 0
|
||||
return s
|
||||
}
|
||||
|
||||
for i := 0; i < len(str); i++ {
|
||||
char := str[i]
|
||||
switch {
|
||||
case escaping:
|
||||
unescapedTrailingSpaces = 0
|
||||
escaping = false
|
||||
switch char {
|
||||
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
|
||||
buffer.WriteByte(char)
|
||||
continue
|
||||
}
|
||||
// Not a special character, assume hex encoded octet
|
||||
if len(str) == i+1 {
|
||||
return nil, errors.New("got corrupted escaped character")
|
||||
}
|
||||
|
||||
dst := []byte{0}
|
||||
n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode escaped character: %s", err)
|
||||
} else if n != 1 {
|
||||
return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
|
||||
}
|
||||
buffer.WriteByte(dst[0])
|
||||
i++
|
||||
case char == '\\':
|
||||
unescapedTrailingSpaces = 0
|
||||
escaping = true
|
||||
case char == '=':
|
||||
attribute.Type = stringFromBuffer()
|
||||
// Special case: If the first character in the value is # the following data
|
||||
// is BER encoded. Throw an error since not supported right now.
|
||||
if len(str) > i+1 && str[i+1] == '#' {
|
||||
return nil, errors.New("unsupported BER encoding")
|
||||
}
|
||||
case char == ',' || char == '+':
|
||||
// We're done with this RDN or value, push it
|
||||
if len(attribute.Type) == 0 {
|
||||
return nil, errors.New("incomplete type, value pair")
|
||||
}
|
||||
attribute.Value = stringFromBuffer()
|
||||
rdn.Attributes = append(rdn.Attributes, attribute)
|
||||
attribute = new(AttributeTypeAndValue)
|
||||
if char == ',' {
|
||||
dn.RDNs = append(dn.RDNs, rdn)
|
||||
rdn = new(RelativeDN)
|
||||
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
|
||||
}
|
||||
case char == ' ' && buffer.Len() == 0:
|
||||
// ignore unescaped leading spaces
|
||||
continue
|
||||
default:
|
||||
if char == ' ' {
|
||||
// Track unescaped spaces in case they are trailing and we need to remove them
|
||||
unescapedTrailingSpaces++
|
||||
} else {
|
||||
// Reset if we see a non-space char
|
||||
unescapedTrailingSpaces = 0
|
||||
}
|
||||
buffer.WriteByte(char)
|
||||
}
|
||||
}
|
||||
if buffer.Len() > 0 {
|
||||
if len(attribute.Type) == 0 {
|
||||
return nil, errors.New("DN ended with incomplete type, value pair")
|
||||
}
|
||||
attribute.Value = stringFromBuffer()
|
||||
rdn.Attributes = append(rdn.Attributes, attribute)
|
||||
dn.RDNs = append(dn.RDNs, rdn)
|
||||
}
|
||||
return dn, nil
|
||||
}
|
||||
|
||||
// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
|
||||
// Returns true if they have the same number of relative distinguished names
|
||||
// and corresponding relative distinguished names (by position) are the same.
|
||||
func (d *DN) Equal(other *DN) bool {
|
||||
if len(d.RDNs) != len(other.RDNs) {
|
||||
return false
|
||||
}
|
||||
for i := range d.RDNs {
|
||||
if !d.RDNs[i].Equal(other.RDNs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RDNsMatch returns true if the individual RDNs of the DNs
|
||||
// are the same regardless of ordering.
|
||||
func (d *DN) RDNsMatch(other *DN) bool {
|
||||
if len(d.RDNs) != len(other.RDNs) {
|
||||
return false
|
||||
}
|
||||
matched := make([]bool, len(other.RDNs))
|
||||
for _, irdn := range d.RDNs {
|
||||
found := false
|
||||
for j, ordn := range other.RDNs {
|
||||
if !matched[j] && irdn.Equal(ordn) {
|
||||
matched[j] = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
|
||||
// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
|
||||
// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
|
||||
// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
|
||||
func (d *DN) AncestorOf(other *DN) bool {
|
||||
if len(d.RDNs) >= len(other.RDNs) {
|
||||
return false
|
||||
}
|
||||
// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
|
||||
otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
|
||||
for i := range d.RDNs {
|
||||
if !d.RDNs[i].Equal(otherRDNs[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
|
||||
// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
|
||||
// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
|
||||
// The order of attributes is not significant.
|
||||
// Case of attribute types is not significant.
|
||||
func (r *RelativeDN) Equal(other *RelativeDN) bool {
|
||||
if len(r.Attributes) != len(other.Attributes) {
|
||||
return false
|
||||
}
|
||||
return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
|
||||
}
|
||||
|
||||
func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
|
||||
for _, attr := range attrs {
|
||||
found := false
|
||||
for _, myattr := range r.Attributes {
|
||||
if myattr.Equal(attr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
|
||||
// Case of the attribute type is not significant
|
||||
func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
|
||||
return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
|
||||
}
|
||||
225
internal/ldap/dn_test.go
Normal file
225
internal/ldap/dn_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
|
||||
// Portions copyright (c) 2015-2016 go-ldap Authors
|
||||
// Static-Check Fixes Copyright 2024 The NATS Authors
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSuccessfulDNParsing(t *testing.T) {
|
||||
testcases := map[string]DN{
|
||||
"": {[]*RelativeDN{}},
|
||||
"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
|
||||
{[]*AttributeTypeAndValue{{"dc", "dummy"}}},
|
||||
{[]*AttributeTypeAndValue{{"dc", "com"}}}}},
|
||||
"UID=jsmith,DC=example,DC=net": {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{"UID", "jsmith"}}},
|
||||
{[]*AttributeTypeAndValue{{"DC", "example"}}},
|
||||
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||
"OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{
|
||||
{"OU", "Sales"},
|
||||
{"CN", "J. Smith"}}},
|
||||
{[]*AttributeTypeAndValue{{"DC", "example"}}},
|
||||
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||
//
|
||||
// "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{
|
||||
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
|
||||
// "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{
|
||||
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}},
|
||||
// {[]*AttributeTypeAndValue{{"DC", "net"}}}}},
|
||||
"CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
|
||||
" CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
|
||||
` A = 1 , B = 2 `: {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{"A", "1"}}},
|
||||
{[]*AttributeTypeAndValue{{"B", "2"}}}}},
|
||||
` A = 1 + B = 2 `: {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{
|
||||
{"A", "1"},
|
||||
{"B", "2"}}}}},
|
||||
` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{{" A ", " 1 "}}},
|
||||
{[]*AttributeTypeAndValue{{" B ", " 2 "}}}}},
|
||||
` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
|
||||
{[]*AttributeTypeAndValue{
|
||||
{" A ", " 1 "},
|
||||
{" B ", " 2 "}}}}},
|
||||
}
|
||||
|
||||
for test, answer := range testcases {
|
||||
dn, err := ParseDN(test)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(dn, &answer) {
|
||||
t.Errorf("Parsed DN %s is not equal to the expected structure", test)
|
||||
t.Logf("Expected:")
|
||||
for _, rdn := range answer.RDNs {
|
||||
for _, attribs := range rdn.Attributes {
|
||||
t.Logf("#%v\n", attribs)
|
||||
}
|
||||
}
|
||||
t.Logf("Actual:")
|
||||
for _, rdn := range dn.RDNs {
|
||||
for _, attribs := range rdn.Attributes {
|
||||
t.Logf("#%v\n", attribs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorDNParsing(t *testing.T) {
|
||||
testcases := map[string]string{
|
||||
"*": "DN ended with incomplete type, value pair",
|
||||
"cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
|
||||
"cn=Jim\\0": "got corrupted escaped character",
|
||||
"DC=example,=net": "DN ended with incomplete type, value pair",
|
||||
// "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string",
|
||||
"test,DC=example,DC=com": "incomplete type, value pair",
|
||||
"=test,DC=example,DC=com": "incomplete type, value pair",
|
||||
}
|
||||
|
||||
for test, answer := range testcases {
|
||||
_, err := ParseDN(test)
|
||||
if err == nil {
|
||||
t.Errorf("Expected %s to fail parsing but succeeded\n", test)
|
||||
} else if err.Error() != answer {
|
||||
t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNEqual(t *testing.T) {
|
||||
testcases := []struct {
|
||||
A string
|
||||
B string
|
||||
Equal bool
|
||||
}{
|
||||
// Exact match
|
||||
{"", "", true},
|
||||
{"o=A", "o=A", true},
|
||||
{"o=A", "o=B", false},
|
||||
|
||||
{"o=A,o=B", "o=A,o=B", true},
|
||||
{"o=A,o=B", "o=A,o=C", false},
|
||||
|
||||
{"o=A+o=B", "o=A+o=B", true},
|
||||
{"o=A+o=B", "o=A+o=C", false},
|
||||
|
||||
// Case mismatch in type is ignored
|
||||
{"o=A", "O=A", true},
|
||||
{"o=A,o=B", "o=A,O=B", true},
|
||||
{"o=A+o=B", "o=A+O=B", true},
|
||||
|
||||
// Case mismatch in value is significant
|
||||
{"o=a", "O=A", false},
|
||||
{"o=a,o=B", "o=A,O=B", false},
|
||||
{"o=a+o=B", "o=A+O=B", false},
|
||||
|
||||
// Multi-valued RDN order mismatch is ignored
|
||||
{"o=A+o=B", "O=B+o=A", true},
|
||||
// Number of RDN attributes is significant
|
||||
{"o=A+o=B", "O=B+o=A+O=B", false},
|
||||
|
||||
// Missing values are significant
|
||||
{"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter
|
||||
{"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter
|
||||
|
||||
// Whitespace tests
|
||||
// Matching
|
||||
{
|
||||
"cn=John Doe, ou=People, dc=sun.com",
|
||||
"cn=John Doe, ou=People, dc=sun.com",
|
||||
true,
|
||||
},
|
||||
// Difference in leading/trailing chars is ignored
|
||||
{
|
||||
"cn=John Doe, ou=People, dc=sun.com",
|
||||
"cn=John Doe,ou=People,dc=sun.com",
|
||||
true,
|
||||
},
|
||||
// Difference in values is significant
|
||||
{
|
||||
"cn=John Doe, ou=People, dc=sun.com",
|
||||
"cn=John Doe, ou=People, dc=sun.com",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testcases {
|
||||
a, err := ParseDN(tc.A)
|
||||
if err != nil {
|
||||
t.Errorf("%d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
b, err := ParseDN(tc.B)
|
||||
if err != nil {
|
||||
t.Errorf("%d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
|
||||
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||
continue
|
||||
}
|
||||
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
|
||||
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNAncestor(t *testing.T) {
|
||||
testcases := []struct {
|
||||
A string
|
||||
B string
|
||||
Ancestor bool
|
||||
}{
|
||||
// Exact match returns false
|
||||
{"", "", false},
|
||||
{"o=A", "o=A", false},
|
||||
{"o=A,o=B", "o=A,o=B", false},
|
||||
{"o=A+o=B", "o=A+o=B", false},
|
||||
|
||||
// Mismatch
|
||||
{"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false},
|
||||
|
||||
// Descendant
|
||||
{"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true},
|
||||
}
|
||||
|
||||
for i, tc := range testcases {
|
||||
a, err := ParseDN(tc.A)
|
||||
if err != nil {
|
||||
t.Errorf("%d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
b, err := ParseDN(tc.B)
|
||||
if err != nil {
|
||||
t.Errorf("%d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual {
|
||||
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNReverseMatchDuplicates(t *testing.T) {
|
||||
// A has two CN=Alice RDNs and one O=NATS.
|
||||
// B has one CN=Alice, one O=NATS, and one OU=Users.
|
||||
// They have the same length (3).
|
||||
dnA, _ := ParseDN("CN=Alice,CN=Alice,O=NATS")
|
||||
dnB, _ := ParseDN("CN=Alice,O=NATS,OU=Users")
|
||||
if dnA.RDNsMatch(dnB) {
|
||||
t.Fatal("Expected RDNsMatch to return false for different RDN sets, but it returned true")
|
||||
}
|
||||
}
|
||||
276
internal/ocsp/ocsp.go
Normal file
276
internal/ocsp/ocsp.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2019-2024 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testhelper
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultResponseTTL = 4 * time.Second
|
||||
defaultAddress = "127.0.0.1:8888"
|
||||
)
|
||||
|
||||
func NewOCSPResponderCustomAddress(t *testing.T, issuerCertPEM, issuerKeyPEM string, addr string) *http.Server {
|
||||
t.Helper()
|
||||
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, addr, defaultResponseTTL, "")
|
||||
}
|
||||
|
||||
func NewOCSPResponder(t *testing.T, issuerCertPEM, issuerKeyPEM string) *http.Server {
|
||||
t.Helper()
|
||||
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, "")
|
||||
}
|
||||
|
||||
func NewOCSPResponderDesignatedCustomAddress(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, addr string) *http.Server {
|
||||
t.Helper()
|
||||
return NewOCSPResponderBase(t, issuerCertPEM, respCertPEM, respKeyPEM, true, addr, defaultResponseTTL, "")
|
||||
}
|
||||
|
||||
func NewOCSPResponderPreferringHTTPMethod(t *testing.T, issuerCertPEM, issuerKeyPEM, method string) *http.Server {
|
||||
t.Helper()
|
||||
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, method)
|
||||
}
|
||||
|
||||
func NewOCSPResponderCustomTimeout(t *testing.T, issuerCertPEM, issuerKeyPEM string, responseTTL time.Duration) *http.Server {
|
||||
t.Helper()
|
||||
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, responseTTL, "")
|
||||
}
|
||||
|
||||
func NewOCSPResponderBase(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, embed bool, addr string, responseTTL time.Duration, method string) *http.Server {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
status := make(map[string]int)
|
||||
|
||||
issuerCert := parseCertPEM(t, issuerCertPEM)
|
||||
respCert := parseCertPEM(t, respCertPEM)
|
||||
respKey := parseKeyPEM(t, respKeyPEM)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// The "/statuses/" endpoint is for directly setting a key-value pair in
|
||||
// the CA's status database.
|
||||
mux.HandleFunc("/statuses/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
key := r.URL.Path[len("/statuses/"):]
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
mu.Lock()
|
||||
n, ok := status[key]
|
||||
if !ok {
|
||||
n = ocsp.Unknown
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
fmt.Fprintf(rw, "%s %d", key, n)
|
||||
case "POST":
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(string(data))
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
status[key] = n
|
||||
mu.Unlock()
|
||||
|
||||
fmt.Fprintf(rw, "%s %d", key, n)
|
||||
default:
|
||||
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
})
|
||||
// The "/" endpoint is for normal OCSP requests. This actually parses an
|
||||
// OCSP status request and signs a response with a CA. Lightly based off:
|
||||
// https://www.ietf.org/rfc/rfc2560.txt
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
var reqData []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
if method != "" && r.Method != method {
|
||||
http.Error(rw, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
reqData, err = base64.StdEncoding.DecodeString(r.URL.Path[1:])
|
||||
case r.Method == "POST":
|
||||
if method != "" && r.Method != method {
|
||||
http.Error(rw, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
reqData, err = io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
default:
|
||||
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(reqData)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
n, ok := status[ocspReq.SerialNumber.String()]
|
||||
if !ok {
|
||||
n = ocsp.Unknown
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
tmpl := ocsp.Response{
|
||||
Status: n,
|
||||
SerialNumber: ocspReq.SerialNumber,
|
||||
ThisUpdate: time.Now(),
|
||||
}
|
||||
if responseTTL != 0 {
|
||||
tmpl.NextUpdate = tmpl.ThisUpdate.Add(responseTTL)
|
||||
}
|
||||
if embed {
|
||||
tmpl.Certificate = respCert
|
||||
}
|
||||
respData, err := ocsp.CreateResponse(issuerCert, respCert, tmpl, respKey)
|
||||
if err != nil {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/ocsp-response")
|
||||
rw.Header().Set("Content-Length", fmt.Sprint(len(respData)))
|
||||
|
||||
fmt.Fprint(rw, string(respData))
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: time.Second * 5,
|
||||
}
|
||||
go srv.ListenAndServe()
|
||||
time.Sleep(1 * time.Second)
|
||||
return srv
|
||||
}
|
||||
|
||||
func parseCertPEM(t *testing.T, certPEM string) *x509.Certificate {
|
||||
t.Helper()
|
||||
block := parsePEM(t, certPEM)
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse cert '%s': %s", certPEM, err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func parseKeyPEM(t *testing.T, keyPEM string) crypto.Signer {
|
||||
t.Helper()
|
||||
block := parsePEM(t, keyPEM)
|
||||
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse ikey %s: %s", keyPEM, err)
|
||||
}
|
||||
}
|
||||
keyc := key.(crypto.Signer)
|
||||
return keyc
|
||||
}
|
||||
|
||||
func parsePEM(t *testing.T, pemPath string) *pem.Block {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(pemPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
t.Fatalf("failed to decode PEM %s", pemPath)
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
|
||||
if len(s.VerifiedChains) == 0 {
|
||||
return nil, fmt.Errorf("missing TLS verified chains")
|
||||
}
|
||||
chain := s.VerifiedChains[0]
|
||||
|
||||
if got, want := len(chain), 2; got < want {
|
||||
return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
|
||||
}
|
||||
leaf, issuer := chain[0], chain[1]
|
||||
|
||||
resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
|
||||
}
|
||||
if err := resp.CheckSignatureFrom(issuer); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
|
||||
t.Helper()
|
||||
|
||||
cert := parseCertPEM(t, certPEM)
|
||||
|
||||
hc := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := hc.Post(
|
||||
fmt.Sprintf("%s/statuses/%s", ocspURL, cert.SerialNumber),
|
||||
"",
|
||||
strings.NewReader(fmt.Sprint(status)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read OCSP HTTP response body: %s", err)
|
||||
}
|
||||
|
||||
if got, want := resp.Status, "200 OK"; got != want {
|
||||
t.Error(strings.TrimSpace(string(data)))
|
||||
t.Fatalf("unexpected OCSP HTTP set status, got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
127
internal/testhelper/logging.go
Normal file
127
internal/testhelper/logging.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2019-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testhelper
|
||||
|
||||
// These routines need to be accessible in both the server and test
|
||||
// directories, and tests importing a package don't get exported symbols from
|
||||
// _test.go files in the imported package, so we put them here where they can
|
||||
// be used freely.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type DummyLogger struct {
|
||||
sync.Mutex
|
||||
Msg string
|
||||
AllMsgs []string
|
||||
}
|
||||
|
||||
func (l *DummyLogger) CheckContent(t *testing.T, expectedStr string) {
|
||||
t.Helper()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
if l.Msg != expectedStr {
|
||||
t.Fatalf("Expected log to be: %v, got %v", expectedStr, l.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DummyLogger) aggregate() {
|
||||
if l.AllMsgs != nil {
|
||||
l.AllMsgs = append(l.AllMsgs, l.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DummyLogger) Noticef(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
func (l *DummyLogger) Errorf(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
func (l *DummyLogger) Warnf(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
func (l *DummyLogger) Fatalf(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
func (l *DummyLogger) Debugf(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
func (l *DummyLogger) Tracef(format string, v ...any) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
l.Msg = fmt.Sprintf(format, v...)
|
||||
l.aggregate()
|
||||
}
|
||||
|
||||
// NewDummyLogger creates a dummy logger and allows to ask for logs to be
|
||||
// retained instead of just keeping the most recent. Use retain to provide an
|
||||
// initial size estimate on messages (not to provide a max capacity).
|
||||
func NewDummyLogger(retain uint) *DummyLogger {
|
||||
l := &DummyLogger{}
|
||||
if retain > 0 {
|
||||
l.AllMsgs = make([]string, 0, retain)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *DummyLogger) Drain() {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
if l.AllMsgs == nil {
|
||||
return
|
||||
}
|
||||
l.AllMsgs = make([]string, 0, len(l.AllMsgs))
|
||||
}
|
||||
|
||||
func (l *DummyLogger) CheckForProhibited(t *testing.T, reason, needle string) {
|
||||
t.Helper()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
if l.AllMsgs == nil {
|
||||
t.Fatal("DummyLogger.CheckForProhibited called without AllMsgs being collected")
|
||||
}
|
||||
|
||||
// Collect _all_ matches, rather than have to re-test repeatedly.
|
||||
// This will particularly help with less deterministic tests with multiple matches.
|
||||
shouldFail := false
|
||||
for i := range l.AllMsgs {
|
||||
if strings.Contains(l.AllMsgs[i], needle) {
|
||||
t.Errorf("log contains %s: %v", reason, l.AllMsgs[i])
|
||||
shouldFail = true
|
||||
}
|
||||
}
|
||||
if shouldFail {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user