github.com/grafana/pyroscope@v1.18.0/pkg/adhocprofiles/adhocprofiles_test.go (about)

     1  package adhocprofiles
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"os"
     9  	"strings"
    10  	"testing"
    11  
    12  	"connectrpc.com/connect"
    13  	"github.com/stretchr/testify/require"
    14  	thanosobjstore "github.com/thanos-io/objstore"
    15  
    16  	v1 "github.com/grafana/pyroscope/api/gen/proto/go/adhocprofiles/v1"
    17  	phlareobjstore "github.com/grafana/pyroscope/pkg/objstore"
    18  	"github.com/grafana/pyroscope/pkg/tenant"
    19  	"github.com/grafana/pyroscope/pkg/util"
    20  	"github.com/grafana/pyroscope/pkg/validation"
    21  )
    22  
    23  func TestAdHocProfiles_Get(t *testing.T) {
    24  	bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket())
    25  	rawProfile, err := os.ReadFile("testdata/cpu.pprof")
    26  	require.NoError(t, err)
    27  	encodedProfile := base64.StdEncoding.EncodeToString(rawProfile)
    28  	ahp := &AdHocProfile{
    29  		Name: "cpu.pprof",
    30  		Data: encodedProfile,
    31  	}
    32  	jsonProfile, _ := json.Marshal(ahp)
    33  	_ = bucket.Upload(context.Background(), "tenant/adhoc/existing-invalid-json", bytes.NewReader([]byte{1, 2, 3}))
    34  	_ = bucket.Upload(context.Background(), "tenant/adhoc/existing-valid-profile", bytes.NewReader(jsonProfile))
    35  	type args struct {
    36  		ctx context.Context
    37  		c   *connect.Request[v1.AdHocProfilesGetRequest]
    38  	}
    39  	tests := []struct {
    40  		name    string
    41  		args    args
    42  		wantErr bool
    43  	}{
    44  		{
    45  			name: "reject requests with missing tenant id",
    46  			args: args{
    47  				ctx: context.Background(),
    48  				c:   nil,
    49  			},
    50  			wantErr: true,
    51  		},
    52  		{
    53  			name: "return error when getting a non existing profile",
    54  			args: args{
    55  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
    56  				c:   connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "non-existing-id"}),
    57  			},
    58  			wantErr: true,
    59  		},
    60  		{
    61  			name: "return error when getting an existing invalid profile",
    62  			args: args{
    63  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
    64  				c:   connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "existing-invalid-profile"}),
    65  			},
    66  			wantErr: true,
    67  		},
    68  		{
    69  			name: "return data when getting an existing valid profile",
    70  			args: args{
    71  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
    72  				c:   connect.NewRequest(&v1.AdHocProfilesGetRequest{Id: "existing-valid-profile"}),
    73  			},
    74  			wantErr: false,
    75  		},
    76  	}
    77  	for _, tt := range tests {
    78  		t.Run(tt.name, func(t *testing.T) {
    79  			a := &AdHocProfiles{
    80  				logger: util.Logger,
    81  				limits: validation.MockLimits{MaxFlameGraphNodesDefaultValue: 8192},
    82  				bucket: bucket,
    83  			}
    84  			_, err := a.Get(tt.args.ctx, tt.args.c)
    85  			if (err != nil) != tt.wantErr {
    86  				t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
    87  				return
    88  			}
    89  		})
    90  	}
    91  }
    92  
    93  func TestAdHocProfiles_List(t *testing.T) {
    94  	bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket())
    95  	_ = bucket.Upload(context.Background(), "tenant/adhoc/bad-id-should-be-ignored", bytes.NewReader([]byte{1}))
    96  	_ = bucket.Upload(context.Background(), "tenant/adhoc/01HMXV8BF4EH71NBYZNPPVGJ2X-cpu.pprof", bytes.NewReader([]byte{1}))
    97  	_ = bucket.Upload(context.Background(), "tenant/adhoc/01HMXRV02963FK36GGRE9N6MPH-heap.pprof", bytes.NewReader([]byte{1}))
    98  	a := &AdHocProfiles{
    99  		logger: util.Logger,
   100  		bucket: bucket,
   101  	}
   102  	response, err := a.List(tenant.InjectTenantID(context.Background(), "tenant"), connect.NewRequest(&v1.AdHocProfilesListRequest{}))
   103  	require.NoError(t, err)
   104  	expected := []*v1.AdHocProfilesProfileMetadata{
   105  		{
   106  			Id:         "01HMXV8BF4EH71NBYZNPPVGJ2X-cpu.pprof",
   107  			Name:       "cpu.pprof",
   108  			UploadedAt: 1706103680484,
   109  		},
   110  		{
   111  			Id:         "01HMXRV02963FK36GGRE9N6MPH-heap.pprof",
   112  			Name:       "heap.pprof",
   113  			UploadedAt: 1706101145673,
   114  		},
   115  	}
   116  	require.Equal(t, connect.NewResponse(&v1.AdHocProfilesListResponse{Profiles: expected}), response)
   117  }
   118  
   119  func TestAdHocProfiles_Upload(t *testing.T) {
   120  	overrides := validation.MockOverrides(func(defaults *validation.Limits, tenantLimits map[string]*validation.Limits) {
   121  		defaults.MaxFlameGraphNodesDefault = 8192
   122  
   123  		l := validation.MockDefaultLimits()
   124  		l.MaxProfileSizeBytes = 16
   125  		tenantLimits["tenant-16-bytes-limit"] = l
   126  
   127  		l = validation.MockDefaultLimits()
   128  		l.MaxProfileSizeBytes = 1600
   129  		tenantLimits["tenant-1600-bytes-limit"] = l
   130  	})
   131  
   132  	bucket := phlareobjstore.NewBucket(thanosobjstore.NewInMemBucket())
   133  	rawProfile, err := os.ReadFile("testdata/cpu.pprof")
   134  	require.NoError(t, err)
   135  	encodedProfile := base64.StdEncoding.EncodeToString(rawProfile)
   136  	type args struct {
   137  		ctx context.Context
   138  		c   *connect.Request[v1.AdHocProfilesUploadRequest]
   139  	}
   140  	tests := []struct {
   141  		name           string
   142  		args           args
   143  		wantErr        string
   144  		expectedSuffix string
   145  	}{
   146  		{
   147  			name: "reject requests with missing tenant id",
   148  			args: args{
   149  				ctx: context.Background(),
   150  				c:   nil,
   151  			},
   152  			wantErr: "no org id",
   153  		},
   154  		{
   155  			name: "should reject an invalid profile",
   156  			args: args{
   157  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
   158  				c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{
   159  					Name:    "test",
   160  					Profile: "123",
   161  				}),
   162  			},
   163  			wantErr: "failed to parse profile",
   164  		},
   165  		{
   166  			name: "should store a valid profile",
   167  			args: args{
   168  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
   169  				c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{
   170  					Name:    "test.cpu.pb.gz",
   171  					Profile: encodedProfile,
   172  				}),
   173  			},
   174  			expectedSuffix: "-test.cpu.pb.gz",
   175  		},
   176  		{
   177  			name: "should limit profile names to particular character set",
   178  			args: args{
   179  				ctx: tenant.InjectTenantID(context.Background(), "tenant"),
   180  				c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{
   181  					Name:    "test/../../../etc/passwd",
   182  					Profile: encodedProfile,
   183  				}),
   184  			},
   185  			expectedSuffix: "-test_.._.._.._etc_passwd",
   186  		},
   187  		{
   188  			name: "should enforce profile size",
   189  			args: args{
   190  				ctx: tenant.InjectTenantID(context.Background(), "tenant-16-bytes-limit"),
   191  				c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{
   192  					Name:    "compressed-too-big",
   193  					Profile: encodedProfile,
   194  				}),
   195  			},
   196  			wantErr: "invalid_argument: profile payload size exceeds limit of 16 B",
   197  		},
   198  		{
   199  			name: "should enforce profile size limit after decompression",
   200  			args: args{
   201  				// 1580 is the profile size compressed
   202  				ctx: tenant.InjectTenantID(context.Background(), "tenant-1600-bytes-limit"),
   203  				c: connect.NewRequest(&v1.AdHocProfilesUploadRequest{
   204  					Name:    "decompressed-too-big",
   205  					Profile: encodedProfile,
   206  				}),
   207  			},
   208  			wantErr: "invalid_argument: uncompressed profile payload size exceeds limit of 1.6 kB",
   209  		},
   210  	}
   211  	for _, tt := range tests {
   212  		t.Run(tt.name, func(t *testing.T) {
   213  			a := &AdHocProfiles{
   214  				logger: util.Logger,
   215  				limits: overrides,
   216  				bucket: bucket,
   217  			}
   218  			_, err := a.Upload(tt.args.ctx, tt.args.c)
   219  			if tt.wantErr == "" {
   220  				require.NoError(t, err)
   221  			} else {
   222  				require.ErrorContains(t, err, tt.wantErr)
   223  			}
   224  
   225  			if tt.expectedSuffix != "" {
   226  				found := false
   227  				err := bucket.Iter(tt.args.ctx, "tenant/adhoc", func(name string) error {
   228  					if strings.HasSuffix(name, tt.expectedSuffix) {
   229  						found = true
   230  					}
   231  					return nil
   232  				})
   233  				require.NoError(t, err)
   234  				require.True(t, found)
   235  			}
   236  		})
   237  	}
   238  }