mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1001 lines
23 KiB
Go
1001 lines
23 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_drive
|
|
|
|
package ipnlocal
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"tailscale.com/drive"
|
|
"tailscale.com/types/views"
|
|
)
|
|
|
|
// TestDriveShareViewsEqual_NilPointer tests nil pointer comparison
|
|
func TestDriveShareViewsEqual_NilPointer(t *testing.T) {
|
|
shares := views.SliceOfViews([]*drive.Share{
|
|
{Name: "test"},
|
|
})
|
|
|
|
if driveShareViewsEqual(nil, shares) {
|
|
t.Error("driveShareViewsEqual(nil, shares) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_EmptySlices tests empty slice comparison
|
|
func TestDriveShareViewsEqual_EmptySlices(t *testing.T) {
|
|
a := views.SliceOfViews([]*drive.Share{})
|
|
b := views.SliceOfViews([]*drive.Share{})
|
|
|
|
if !driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(empty, empty) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_DifferentLengths tests different length slices
|
|
func TestDriveShareViewsEqual_DifferentLengths(t *testing.T) {
|
|
a := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1"},
|
|
})
|
|
b := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1"},
|
|
{Name: "share2"},
|
|
})
|
|
|
|
if driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(len=1, len=2) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_SameSingleShare tests identical single share
|
|
func TestDriveShareViewsEqual_SameSingleShare(t *testing.T) {
|
|
share := &drive.Share{
|
|
Name: "test",
|
|
Path: "/path/to/test",
|
|
}
|
|
|
|
a := views.SliceOfViews([]*drive.Share{share})
|
|
b := views.SliceOfViews([]*drive.Share{share})
|
|
|
|
if !driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(same, same) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_DifferentShares tests different shares
|
|
func TestDriveShareViewsEqual_DifferentShares(t *testing.T) {
|
|
a := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1", Path: "/path1"},
|
|
})
|
|
b := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share2", Path: "/path2"},
|
|
})
|
|
|
|
if driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(different, different) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_MultipleShares tests multiple identical shares
|
|
func TestDriveShareViewsEqual_MultipleShares(t *testing.T) {
|
|
shares := []*drive.Share{
|
|
{Name: "share1", Path: "/path1"},
|
|
{Name: "share2", Path: "/path2"},
|
|
{Name: "share3", Path: "/path3"},
|
|
}
|
|
|
|
a := views.SliceOfViews(shares)
|
|
b := views.SliceOfViews(shares)
|
|
|
|
if !driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(same3, same3) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_DifferentOrder tests shares in different order
|
|
func TestDriveShareViewsEqual_DifferentOrder(t *testing.T) {
|
|
a := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1", Path: "/path1"},
|
|
{Name: "share2", Path: "/path2"},
|
|
})
|
|
b := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share2", Path: "/path2"},
|
|
{Name: "share1", Path: "/path1"},
|
|
})
|
|
|
|
if driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(different order) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_SameOrder tests shares in same order
|
|
func TestDriveShareViewsEqual_SameOrder(t *testing.T) {
|
|
shares := []*drive.Share{
|
|
{Name: "a", Path: "/a"},
|
|
{Name: "b", Path: "/b"},
|
|
{Name: "c", Path: "/c"},
|
|
}
|
|
|
|
a := views.SliceOfViews(shares)
|
|
b := views.SliceOfViews(shares)
|
|
|
|
if !driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(same order) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_OneShareDifferent tests one share different
|
|
func TestDriveShareViewsEqual_OneShareDifferent(t *testing.T) {
|
|
a := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1", Path: "/path1"},
|
|
{Name: "share2", Path: "/path2"},
|
|
{Name: "share3", Path: "/path3"},
|
|
})
|
|
b := views.SliceOfViews([]*drive.Share{
|
|
{Name: "share1", Path: "/path1"},
|
|
{Name: "share2", Path: "/path_modified"},
|
|
{Name: "share3", Path: "/path3"},
|
|
})
|
|
|
|
if driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(one different) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_Read tests Read method
|
|
func TestResponseBodyWrapper_Read(t *testing.T) {
|
|
data := "test data for reading"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, len(data))
|
|
n, err := rbw.Read(buf)
|
|
|
|
if err != nil && err != io.EOF {
|
|
t.Fatalf("Read() error = %v, want nil or EOF", err)
|
|
}
|
|
|
|
if n != len(data) {
|
|
t.Errorf("Read() n = %d, want %d", n, len(data))
|
|
}
|
|
|
|
if rbw.bytesRx != int64(len(data)) {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, len(data))
|
|
}
|
|
|
|
if string(buf) != data {
|
|
t.Errorf("Read() data = %q, want %q", buf, data)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ReadMultiple tests multiple Read calls
|
|
func TestResponseBodyWrapper_ReadMultiple(t *testing.T) {
|
|
data := "abcdefghijklmnopqrstuvwxyz"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
// Read in chunks
|
|
buf1 := make([]byte, 10)
|
|
n1, _ := rbw.Read(buf1)
|
|
|
|
buf2 := make([]byte, 10)
|
|
n2, _ := rbw.Read(buf2)
|
|
|
|
totalRead := int64(n1 + n2)
|
|
if rbw.bytesRx != totalRead {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, totalRead)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ReadError tests Read with error
|
|
func TestResponseBodyWrapper_ReadError(t *testing.T) {
|
|
testErr := errors.New("read error")
|
|
|
|
rc := &errorReader{err: testErr}
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, 10)
|
|
_, err := rbw.Read(buf)
|
|
|
|
if err != testErr {
|
|
t.Errorf("Read() error = %v, want %v", err, testErr)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_Close tests Close method
|
|
func TestResponseBodyWrapper_Close(t *testing.T) {
|
|
rc := io.NopCloser(strings.NewReader("test"))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
err := rbw.Close()
|
|
if err != nil {
|
|
t.Errorf("Close() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_CloseWithError tests Close with error
|
|
func TestResponseBodyWrapper_CloseWithError(t *testing.T) {
|
|
testErr := errors.New("close error")
|
|
rc := &errorCloser{err: testErr}
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
err := rbw.Close()
|
|
if err != testErr {
|
|
t.Errorf("Close() error = %v, want %v", err, testErr)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogAccess_NilLogger tests logging with nil logger
|
|
func TestResponseBodyWrapper_LogAccess_NilLogger(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: nil,
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentLength: 1024,
|
|
}
|
|
|
|
// Should not panic
|
|
rbw.logAccess("")
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogAccess_ZeroLength tests zero-length content logging
|
|
func TestResponseBodyWrapper_LogAccess_ZeroLength(t *testing.T) {
|
|
logged := false
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
logged = true
|
|
},
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentLength: 0,
|
|
logVerbose: false,
|
|
}
|
|
|
|
rbw.logAccess("")
|
|
|
|
if logged {
|
|
t.Error("logAccess() logged zero-length non-verbose request, should be silent")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogAccess_VerboseMode tests verbose logging
|
|
func TestResponseBodyWrapper_LogAccess_VerboseMode(t *testing.T) {
|
|
logged := false
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
logged = true
|
|
if !strings.Contains(format, "[v1]") {
|
|
t.Error("verbose log should contain [v1] prefix")
|
|
}
|
|
},
|
|
method: "PROPFIND",
|
|
statusCode: 200,
|
|
contentLength: 0,
|
|
logVerbose: true,
|
|
}
|
|
|
|
rbw.logAccess("")
|
|
|
|
if !logged {
|
|
t.Error("logAccess() did not log in verbose mode")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogAccess_NonZeroContent tests logging non-zero content
|
|
func TestResponseBodyWrapper_LogAccess_NonZeroContent(t *testing.T) {
|
|
logged := false
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
logged = true
|
|
},
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentLength: 1024,
|
|
logVerbose: false,
|
|
}
|
|
|
|
rbw.logAccess("")
|
|
|
|
if !logged {
|
|
t.Error("logAccess() did not log non-zero content")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogAccess_WithError tests logging with error
|
|
func TestResponseBodyWrapper_LogAccess_WithError(t *testing.T) {
|
|
errorLogged := ""
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
// Extract the error from the args
|
|
for _, arg := range args {
|
|
if s, ok := arg.(string); ok && s != "" {
|
|
errorLogged = s
|
|
}
|
|
}
|
|
},
|
|
method: "GET",
|
|
statusCode: 500,
|
|
contentLength: 100,
|
|
}
|
|
|
|
testError := "test error message"
|
|
rbw.logAccess(testError)
|
|
|
|
if errorLogged != testError {
|
|
t.Errorf("logged error = %q, want %q", errorLogged, testError)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ReadThenClose tests typical usage pattern
|
|
func TestResponseBodyWrapper_ReadThenClose(t *testing.T) {
|
|
data := "test data"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
closeLogged := false
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: func(format string, args ...any) {
|
|
closeLogged = true
|
|
},
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentLength: int64(len(data)),
|
|
}
|
|
|
|
// Read all data
|
|
buf := make([]byte, len(data))
|
|
rbw.Read(buf)
|
|
|
|
// Close should log
|
|
rbw.Close()
|
|
|
|
if !closeLogged {
|
|
t.Error("Close() did not log access")
|
|
}
|
|
|
|
if rbw.bytesRx != int64(len(data)) {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, len(data))
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_StatusCodes tests different status codes
|
|
func TestResponseBodyWrapper_StatusCodes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
statusCode int
|
|
wantLogged bool
|
|
}{
|
|
{"success_200", 200, true},
|
|
{"created_201", 201, true},
|
|
{"no_content_204", 204, false}, // Zero content
|
|
{"bad_request_400", 400, true},
|
|
{"not_found_404", 404, true},
|
|
{"server_error_500", 500, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logged := false
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
logged = true
|
|
},
|
|
method: "GET",
|
|
statusCode: tt.statusCode,
|
|
contentLength: 0,
|
|
logVerbose: true, // Force logging
|
|
}
|
|
|
|
rbw.logAccess("")
|
|
|
|
if logged != tt.wantLogged {
|
|
t.Errorf("logged = %v, want %v", logged, tt.wantLogged)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ContentTypes tests different content types
|
|
func TestResponseBodyWrapper_ContentTypes(t *testing.T) {
|
|
tests := []struct {
|
|
contentType string
|
|
}{
|
|
{"text/plain"},
|
|
{"application/json"},
|
|
{"application/octet-stream"},
|
|
{"image/png"},
|
|
{"video/mp4"},
|
|
{""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.contentType, func(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentType: tt.contentType,
|
|
contentLength: 100,
|
|
}
|
|
|
|
// Should not panic
|
|
rbw.logAccess("")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_Methods tests different HTTP methods
|
|
func TestResponseBodyWrapper_Methods(t *testing.T) {
|
|
methods := []string{"GET", "PUT", "POST", "DELETE", "HEAD", "PROPFIND", "MKCOL"}
|
|
|
|
for _, method := range methods {
|
|
t.Run(method, func(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: method,
|
|
statusCode: 200,
|
|
contentLength: 100,
|
|
}
|
|
|
|
// Should not panic
|
|
rbw.logAccess("")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_FileExtensions tests different file extensions
|
|
func TestResponseBodyWrapper_FileExtensions(t *testing.T) {
|
|
extensions := []string{".txt", ".pdf", ".jpg", ".mp4", ".doc", ""}
|
|
|
|
for _, ext := range extensions {
|
|
t.Run(ext, func(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: "GET",
|
|
statusCode: 200,
|
|
fileExtension: ext,
|
|
contentLength: 100,
|
|
}
|
|
|
|
// Should not panic
|
|
rbw.logAccess("")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_TrafficRounding tests traffic rounding
|
|
func TestResponseBodyWrapper_TrafficRounding(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: "GET",
|
|
statusCode: 200,
|
|
contentLength: 1536, // Should round
|
|
bytesRx: 2048, // Should round
|
|
bytesTx: 512, // Should round
|
|
}
|
|
|
|
// Should not panic with large numbers
|
|
rbw.logAccess("")
|
|
}
|
|
|
|
// TestResponseBodyWrapper_NodeKeys tests node key logging
|
|
func TestResponseBodyWrapper_NodeKeys(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: "GET",
|
|
statusCode: 200,
|
|
selfNodeKey: "self123",
|
|
shareNodeKey: "share456",
|
|
contentLength: 100,
|
|
}
|
|
|
|
// Should not panic
|
|
rbw.logAccess("")
|
|
}
|
|
|
|
// TestDriveTransport_RoundTrip_RemovesHeaders tests header removal
|
|
func TestDriveTransport_RoundTrip_RemovesHeaders(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Verify headers are removed
|
|
if r.Header.Get("Origin") != "" {
|
|
t.Error("Origin header not removed")
|
|
}
|
|
if r.Header.Get("Referer") != "" {
|
|
t.Error("Referer header not removed")
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Note: Cannot easily test driveTransport without full LocalBackend setup
|
|
// This is a structural test
|
|
}
|
|
|
|
// TestDriveTransport_RequestBodyWrapper tests request body wrapping
|
|
func TestDriveTransport_RequestBodyWrapper(t *testing.T) {
|
|
// Test the requestBodyWrapper concept
|
|
data := "test request body"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
// Read all data
|
|
buf := make([]byte, len(data))
|
|
n, err := rc.Read(buf)
|
|
|
|
if err != nil && err != io.EOF {
|
|
t.Fatalf("Read() error = %v", err)
|
|
}
|
|
|
|
if n != len(data) {
|
|
t.Errorf("Read() n = %d, want %d", n, len(data))
|
|
}
|
|
|
|
rc.Close()
|
|
}
|
|
|
|
// errorReader is a ReadCloser that always returns an error on Read
|
|
type errorReader struct {
|
|
err error
|
|
}
|
|
|
|
func (er *errorReader) Read(p []byte) (int, error) {
|
|
return 0, er.err
|
|
}
|
|
|
|
func (er *errorReader) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// errorCloser is a ReadCloser that always returns an error on Close
|
|
type errorCloser struct {
|
|
err error
|
|
}
|
|
|
|
func (ec *errorCloser) Read(p []byte) (int, error) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (ec *errorCloser) Close() error {
|
|
return ec.err
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LargeRead tests reading large data
|
|
func TestResponseBodyWrapper_LargeRead(t *testing.T) {
|
|
// Create 1MB of data
|
|
data := make([]byte, 1024*1024)
|
|
for i := range data {
|
|
data[i] = byte(i % 256)
|
|
}
|
|
|
|
rc := io.NopCloser(strings.NewReader(string(data)))
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, len(data))
|
|
n, err := io.ReadFull(rbw, buf)
|
|
|
|
if err != nil {
|
|
t.Fatalf("ReadFull() error = %v", err)
|
|
}
|
|
|
|
if n != len(data) {
|
|
t.Errorf("ReadFull() n = %d, want %d", n, len(data))
|
|
}
|
|
|
|
if rbw.bytesRx != int64(len(data)) {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, len(data))
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_PartialRead tests partial reading
|
|
func TestResponseBodyWrapper_PartialRead(t *testing.T) {
|
|
data := "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
// Read only first 10 bytes
|
|
buf := make([]byte, 10)
|
|
n, err := rbw.Read(buf)
|
|
|
|
if err != nil && err != io.EOF {
|
|
t.Fatalf("Read() error = %v", err)
|
|
}
|
|
|
|
if n != 10 {
|
|
t.Errorf("Read() n = %d, want 10", n)
|
|
}
|
|
|
|
if rbw.bytesRx != 10 {
|
|
t.Errorf("bytesRx = %d, want 10", rbw.bytesRx)
|
|
}
|
|
|
|
// Close should log with only 10 bytes read
|
|
rbw.Close()
|
|
}
|
|
|
|
// TestResponseBodyWrapper_EmptyRead tests reading empty data
|
|
func TestResponseBodyWrapper_EmptyRead(t *testing.T) {
|
|
rc := io.NopCloser(strings.NewReader(""))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, 10)
|
|
n, err := rbw.Read(buf)
|
|
|
|
if err != io.EOF {
|
|
t.Errorf("Read() error = %v, want EOF", err)
|
|
}
|
|
|
|
if n != 0 {
|
|
t.Errorf("Read() n = %d, want 0", n)
|
|
}
|
|
|
|
if rbw.bytesRx != 0 {
|
|
t.Errorf("bytesRx = %d, want 0", rbw.bytesRx)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ReadEOF tests EOF handling
|
|
func TestResponseBodyWrapper_ReadEOF(t *testing.T) {
|
|
data := "short"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, len(data))
|
|
n1, _ := rbw.Read(buf)
|
|
|
|
// Read again to get EOF
|
|
buf2 := make([]byte, 10)
|
|
n2, err := rbw.Read(buf2)
|
|
|
|
if err != io.EOF {
|
|
t.Errorf("second Read() error = %v, want EOF", err)
|
|
}
|
|
|
|
if n2 != 0 {
|
|
t.Errorf("second Read() n = %d, want 0", n2)
|
|
}
|
|
|
|
totalBytes := int64(n1 + n2)
|
|
if rbw.bytesRx != totalBytes {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, totalBytes)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_MultipleClose tests multiple Close calls
|
|
func TestResponseBodyWrapper_MultipleClose(t *testing.T) {
|
|
rc := io.NopCloser(strings.NewReader("test"))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
// First close should succeed
|
|
err1 := rbw.Close()
|
|
if err1 != nil {
|
|
t.Errorf("first Close() error = %v, want nil", err1)
|
|
}
|
|
|
|
// Second close behavior depends on underlying ReadCloser
|
|
// Just verify it doesn't panic
|
|
rbw.Close()
|
|
}
|
|
|
|
// TestResponseBodyWrapper_CloseWithoutRead tests closing without reading
|
|
func TestResponseBodyWrapper_CloseWithoutRead(t *testing.T) {
|
|
rc := io.NopCloser(strings.NewReader("test"))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
// Close without reading
|
|
err := rbw.Close()
|
|
if err != nil {
|
|
t.Errorf("Close() error = %v, want nil", err)
|
|
}
|
|
|
|
if rbw.bytesRx != 0 {
|
|
t.Errorf("bytesRx = %d, want 0 (no reads)", rbw.bytesRx)
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_InterruptedRead tests interrupted reading
|
|
func TestResponseBodyWrapper_InterruptedRead(t *testing.T) {
|
|
data := "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
// Read some data
|
|
buf1 := make([]byte, 10)
|
|
rbw.Read(buf1)
|
|
|
|
// Close before reading all data
|
|
rbw.Close()
|
|
|
|
if rbw.bytesRx != 10 {
|
|
t.Errorf("bytesRx = %d, want 10 (partial read)", rbw.bytesRx)
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_LargeLists tests large share lists
|
|
func TestDriveShareViewsEqual_LargeLists(t *testing.T) {
|
|
// Create 100 shares
|
|
shares := make([]*drive.Share, 100)
|
|
for i := range shares {
|
|
shares[i] = &drive.Share{
|
|
Name: string(rune('a' + i%26)),
|
|
Path: "/path/" + string(rune('a'+i%26)),
|
|
}
|
|
}
|
|
|
|
a := views.SliceOfViews(shares)
|
|
b := views.SliceOfViews(shares)
|
|
|
|
if !driveShareViewsEqual(&a, b) {
|
|
t.Error("driveShareViewsEqual(large, large) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_NilVsEmpty tests nil vs empty slice
|
|
func TestDriveShareViewsEqual_NilVsEmpty(t *testing.T) {
|
|
empty := views.SliceOfViews([]*drive.Share{})
|
|
|
|
// nil pointer vs empty slice
|
|
if driveShareViewsEqual(nil, empty) {
|
|
t.Error("driveShareViewsEqual(nil, empty) = true, want false")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_BytesCounting tests accurate byte counting
|
|
func TestResponseBodyWrapper_BytesCounting(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dataSize int
|
|
}{
|
|
{"small_10", 10},
|
|
{"medium_1024", 1024},
|
|
{"large_10240", 10240},
|
|
{"exact_page_4096", 4096},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
data := make([]byte, tt.dataSize)
|
|
rc := io.NopCloser(strings.NewReader(string(data)))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, tt.dataSize)
|
|
n, _ := io.ReadFull(rbw, buf)
|
|
|
|
if rbw.bytesRx != int64(n) {
|
|
t.Errorf("bytesRx = %d, want %d", rbw.bytesRx, n)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ConcurrentAccess tests concurrent access safety
|
|
func TestResponseBodyWrapper_ConcurrentAccess(t *testing.T) {
|
|
// Note: responseBodyWrapper is not designed for concurrent access
|
|
// This test just ensures no obvious race conditions in single-threaded use
|
|
data := "test data"
|
|
rc := io.NopCloser(strings.NewReader(data))
|
|
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, len(data))
|
|
rbw.Read(buf)
|
|
rbw.Close()
|
|
|
|
// Should complete without race detector warnings
|
|
}
|
|
|
|
// TestResponseBodyWrapper_LogFormat tests log format structure
|
|
func TestResponseBodyWrapper_LogFormat(t *testing.T) {
|
|
formatSeen := ""
|
|
rbw := &responseBodyWrapper{
|
|
log: func(format string, args ...any) {
|
|
formatSeen = format
|
|
},
|
|
method: "GET",
|
|
statusCode: 200,
|
|
selfNodeKey: "self",
|
|
shareNodeKey: "share",
|
|
fileExtension: ".txt",
|
|
contentType: "text/plain",
|
|
contentLength: 100,
|
|
bytesTx: 50,
|
|
bytesRx: 100,
|
|
}
|
|
|
|
rbw.logAccess("no error")
|
|
|
|
// Verify log format contains expected fields
|
|
expectedFields := []string{
|
|
"taildrive: access:",
|
|
"status-code=",
|
|
"ext=",
|
|
"content-type=",
|
|
"content-length=",
|
|
"tx=",
|
|
"rx=",
|
|
"err=",
|
|
}
|
|
|
|
for _, field := range expectedFields {
|
|
if !strings.Contains(formatSeen, field) {
|
|
t.Errorf("log format missing field: %q", field)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_BoundaryConditions tests boundary conditions
|
|
func TestDriveShareViewsEqual_BoundaryConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
aLen int
|
|
bLen int
|
|
equal bool
|
|
}{
|
|
{"zero_zero", 0, 0, true},
|
|
{"zero_one", 0, 1, false},
|
|
{"one_zero", 1, 0, false},
|
|
{"one_one", 1, 1, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
aShares := make([]*drive.Share, tt.aLen)
|
|
bShares := make([]*drive.Share, tt.bLen)
|
|
|
|
for i := range aShares {
|
|
aShares[i] = &drive.Share{Name: "test"}
|
|
}
|
|
for i := range bShares {
|
|
bShares[i] = &drive.Share{Name: "test"}
|
|
}
|
|
|
|
a := views.SliceOfViews(aShares)
|
|
b := views.SliceOfViews(bShares)
|
|
|
|
result := driveShareViewsEqual(&a, b)
|
|
if result != tt.equal {
|
|
t.Errorf("driveShareViewsEqual() = %v, want %v", result, tt.equal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_AllFieldsSet tests all fields are logged
|
|
func TestResponseBodyWrapper_AllFieldsSet(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
logVerbose: true,
|
|
bytesRx: 1024,
|
|
bytesTx: 512,
|
|
method: "PUT",
|
|
statusCode: 201,
|
|
contentType: "application/octet-stream",
|
|
fileExtension: ".bin",
|
|
shareNodeKey: "node123",
|
|
selfNodeKey: "self456",
|
|
contentLength: 2048,
|
|
}
|
|
|
|
// Should not panic with all fields set
|
|
rbw.logAccess("test error")
|
|
}
|
|
|
|
// TestResponseBodyWrapper_MinimalFields tests minimal field set
|
|
func TestResponseBodyWrapper_MinimalFields(t *testing.T) {
|
|
rbw := &responseBodyWrapper{
|
|
log: t.Logf,
|
|
method: "GET",
|
|
contentLength: 100,
|
|
}
|
|
|
|
// Should not panic with minimal fields
|
|
rbw.logAccess("")
|
|
}
|
|
|
|
// TestDriveShareViewsEqual_IdenticalPointers tests same pointer
|
|
func TestDriveShareViewsEqual_IdenticalPointers(t *testing.T) {
|
|
shares := views.SliceOfViews([]*drive.Share{
|
|
{Name: "test"},
|
|
})
|
|
|
|
if !driveShareViewsEqual(&shares, shares) {
|
|
t.Error("driveShareViewsEqual(same ptr, same ptr) = false, want true")
|
|
}
|
|
}
|
|
|
|
// TestResponseBodyWrapper_ReadAfterError tests reading after error
|
|
func TestResponseBodyWrapper_ReadAfterError(t *testing.T) {
|
|
rc := &errorReader{err: errors.New("read error")}
|
|
rbw := &responseBodyWrapper{
|
|
ReadCloser: rc,
|
|
log: t.Logf,
|
|
method: "GET",
|
|
}
|
|
|
|
buf := make([]byte, 10)
|
|
|
|
// First read gets error
|
|
_, err1 := rbw.Read(buf)
|
|
if err1 == nil {
|
|
t.Error("first Read() should return error")
|
|
}
|
|
|
|
// Second read should also get error
|
|
_, err2 := rbw.Read(buf)
|
|
if err2 == nil {
|
|
t.Error("second Read() should return error")
|
|
}
|
|
}
|