// Copyright (C) MongoDB, Inc. 2017-present. // // 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 package ocsp import ( "context" "crypto" "crypto/tls" "errors" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" "go.mongodb.org/mongo-driver/internal/testutil/assert" "golang.org/x/crypto/ocsp" ) var ( ctx = context.Background() ) func TestCache(t *testing.T) { assert.RegisterOpts(reflect.TypeOf(&ocsp.Response{}), cmp.Comparer(func(r1, r2 *ocsp.Response) bool { return r1 == r2 })) testRequest := &ocsp.Request{ HashAlgorithm: crypto.SHA1, IssuerNameHash: []byte("issuerNameHash"), IssuerKeyHash: []byte("issuerKeyHash"), } testRequestKey := createCacheKey(testRequest) t.Run("verification errors if cache is nil", func(t *testing.T) { err := Verify(ctx, tls.ConnectionState{}, &VerifyOptions{}) assert.NotNil(t, err, "expected error, got nil") ocspErr, ok := err.(*Error) assert.True(t, ok, "expected error of type %T, got %v of type %T", &Error{}, err, err) expected := &Error{ wrapped: errors.New("no OCSP cache provided"), } assert.Equal(t, expected.Error(), ocspErr.Error(), "expected error %v, got %v", expected, ocspErr) }) t.Run("put", func(t *testing.T) { t.Run("empty cache", func(t *testing.T) { good := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(10), } revoked := &ResponseDetails{ Status: ocsp.Revoked, NextUpdate: futureTime(10), } unknown := &ResponseDetails{ Status: ocsp.Unknown, NextUpdate: futureTime(10), } goodNoUpdate := &ResponseDetails{Status: ocsp.Good} revokedNoUpdate := &ResponseDetails{Status: ocsp.Revoked} unknownNoUpdate := &ResponseDetails{Status: ocsp.Unknown} testCases := []struct { name string response *ResponseDetails cached bool }{ {"good response is cached", good, true}, {"revoked response is cached", revoked, true}, {"good response without nextUpdate is not cached", goodNoUpdate, false}, {"revoked response without nextUpdate is not cached", revokedNoUpdate, false}, {"unknown response with nextUpdate is not cached", unknown, false}, {"unknown response without nextUpdate is not cached", unknownNoUpdate, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cache := NewCache() putRes := cache.Update(testRequest, tc.response) // Put should always return tc.response even if it wasn't added to the cache because it is the most // up-to-date response. assert.Equal(t, tc.response, putRes, "expected Put to return %v, got %v", tc.response, putRes) current, ok := cache.cache[testRequestKey] if tc.cached { assert.True(t, ok, "expected cache to contain entry but did not") assert.Equal(t, tc.response, current, "expected cache to contain %v, got %v", tc.response, current) return } assert.False(t, ok, "expected cache to contain nil, got %v", current) assert.Nil(t, current, "expected cache to contain no entry, got %v", current) }) } }) t.Run("non-empty cache", func(t *testing.T) { cachedUpdateMinutes := 10 originalCached := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(cachedUpdateMinutes), } unknownUpdate := &ResponseDetails{ Status: ocsp.Unknown, NextUpdate: futureTime(cachedUpdateMinutes + 5), } goodNoUpdate := &ResponseDetails{Status: ocsp.Good} revokedNoUpdate := &ResponseDetails{Status: ocsp.Revoked} goodEarlierUpdate := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(cachedUpdateMinutes - 5), } revokedEarlierUpdate := &ResponseDetails{ Status: ocsp.Revoked, NextUpdate: futureTime(cachedUpdateMinutes - 5), } goodLaterUpdate := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(cachedUpdateMinutes + 5), } revokedLaterUpdate := &ResponseDetails{ Status: ocsp.Revoked, NextUpdate: futureTime(cachedUpdateMinutes + 5), } testCases := []struct { name string response *ResponseDetails putReturn *ResponseDetails cached *ResponseDetails }{ {"unknown with later nextUpdate is not cached", unknownUpdate, originalCached, originalCached}, {"good with no nextUpdate clears cache", goodNoUpdate, goodNoUpdate, nil}, {"revoked with no nextUpdate clears cache", revokedNoUpdate, revokedNoUpdate, nil}, {"good with earlier nextUpdate is not cached", goodEarlierUpdate, originalCached, originalCached}, {"revoked with earlier nextUpdate is not cached", revokedEarlierUpdate, originalCached, originalCached}, {"good with later nextUpdate is cached", goodLaterUpdate, goodLaterUpdate, goodLaterUpdate}, {"revoked with later nextUpdate is cached", revokedLaterUpdate, revokedLaterUpdate, revokedLaterUpdate}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cache := NewCache() _ = cache.Update(testRequest, originalCached) putReturn := cache.Update(testRequest, tc.response) assert.Equal(t, tc.putReturn, putReturn, "expected Put to return %v, got %v", tc.putReturn, putReturn) current, ok := cache.cache[testRequestKey] if tc.cached == nil { assert.False(t, ok, "expected cache to contain no entry, got %v", current) return } assert.True(t, ok, "expected cache to contain %v, got nil", tc.cached) assert.Equal(t, tc.cached, current, "expected cache to contain %v, got %v", tc.cached, current) }) } }) }) t.Run("get", func(t *testing.T) { t.Run("empty cache returns nil", func(t *testing.T) { cache := NewCache() res := cache.Get(testRequest) assert.Nil(t, res, "expected Get to return nil, got %v", res) }) t.Run("valid entry returned", func(t *testing.T) { cache := NewCache() originalResponse := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(10), } cache.cache[testRequestKey] = originalResponse res := cache.Get(testRequest) assert.Equal(t, originalResponse, res, "expected Get to return %v, got %v", originalResponse, res) }) t.Run("expired entry is deleted", func(t *testing.T) { cache := NewCache() originalResponse := &ResponseDetails{ Status: ocsp.Good, NextUpdate: futureTime(-10), } cache.cache[testRequestKey] = originalResponse res := cache.Get(testRequest) assert.Nil(t, res, "expected Get to return nil, got %v", res) cached, ok := cache.cache[testRequestKey] assert.False(t, ok, "expected cache to contain no entry, got %v", cached) }) }) } func futureTime(minutes int) time.Time { return time.Now().Add(time.Duration(minutes) * time.Minute).UTC() }