* Add template api endpoints Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Added template bypass Pools and scale sets will automatically migrate to the new template system for runner install scripts. If a pool or a scale set cannot be migrate, it is left alone. It is expected that users set a runner install template manually for scenarios we don't yet have a template for (windows on gitea for example). Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Integrate templates with pool create/update Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Add webapp integration with templates Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Add unit tests Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Populate all relevant context fields Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Update dependencies Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Fix lint Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Validate uint Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Add CLI template management Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Some editor improvements and bugfixes Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Fix scale set return values post create Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> * Fix template websocket events filter Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com> --------- Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
498 lines
16 KiB
Go
498 lines
16 KiB
Go
// Copyright 2025 Cloudbase Solutions SRL
|
|
//
|
|
// 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 runner
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
|
|
commonParams "github.com/cloudbase/garm-provider-common/params"
|
|
"github.com/cloudbase/garm/auth"
|
|
"github.com/cloudbase/garm/database"
|
|
dbCommon "github.com/cloudbase/garm/database/common"
|
|
garmTesting "github.com/cloudbase/garm/internal/testing"
|
|
"github.com/cloudbase/garm/params"
|
|
"github.com/cloudbase/garm/runner/common"
|
|
runnerCommonMocks "github.com/cloudbase/garm/runner/common/mocks"
|
|
runnerMocks "github.com/cloudbase/garm/runner/mocks"
|
|
)
|
|
|
|
type MetadataTestFixtures struct {
|
|
AdminContext context.Context
|
|
Store dbCommon.Store
|
|
Providers map[string]common.Provider
|
|
ProviderMock *runnerCommonMocks.Provider
|
|
PoolMgrMock *runnerCommonMocks.PoolManager
|
|
PoolMgrCtrlMock *runnerMocks.PoolManagerController
|
|
TestInstance params.Instance
|
|
TestEntity params.ForgeEntity
|
|
TestPool params.Pool
|
|
TestTemplate params.Template
|
|
}
|
|
|
|
type MetadataTestSuite struct {
|
|
suite.Suite
|
|
Fixtures *MetadataTestFixtures
|
|
Runner *Runner
|
|
|
|
adminCtx context.Context
|
|
instanceCtx context.Context
|
|
unauthorizedCtx context.Context
|
|
invalidInstanceCtx context.Context
|
|
jitInstanceCtx context.Context
|
|
githubEndpoint params.ForgeEndpoint
|
|
}
|
|
|
|
func (s *MetadataTestSuite) SetupTest() {
|
|
// create testing sqlite database
|
|
dbCfg := garmTesting.GetTestSqliteDBConfig(s.T())
|
|
db, err := database.NewDatabase(context.Background(), dbCfg)
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to create db connection: %s", err))
|
|
}
|
|
|
|
s.adminCtx = garmTesting.ImpersonateAdminContext(context.Background(), db, s.T())
|
|
|
|
s.githubEndpoint = garmTesting.CreateDefaultGithubEndpoint(s.adminCtx, db, s.T())
|
|
testCreds := garmTesting.CreateTestGithubCredentials(s.adminCtx, "test-creds", db, s.T(), s.githubEndpoint)
|
|
|
|
// Create test organization
|
|
org, err := db.CreateOrganization(s.adminCtx, "test-org", testCreds, "test-webhook-secret", params.PoolBalancerTypeRoundRobin)
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to create test org: %s", err))
|
|
}
|
|
|
|
entity, err := org.GetEntity()
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to get entity: %s", err))
|
|
}
|
|
// Set entity name for service name generation
|
|
entity.Name = "test-org"
|
|
|
|
// Create test template
|
|
template, err := db.CreateTemplate(s.adminCtx, params.CreateTemplateParams{
|
|
Name: "test-template",
|
|
Description: "Test template for metadata tests",
|
|
OSType: commonParams.Linux,
|
|
ForgeType: params.GithubEndpointType,
|
|
Data: []byte(`#!/bin/bash\necho "Installing runner..."`),
|
|
})
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to create test template: %s", err))
|
|
}
|
|
|
|
// Create test pool
|
|
pool, err := db.CreateEntityPool(s.adminCtx, entity, params.CreatePoolParams{
|
|
ProviderName: "test-provider",
|
|
MaxRunners: 2,
|
|
MinIdleRunners: 1,
|
|
Image: "ubuntu:22.04",
|
|
Flavor: "medium",
|
|
OSType: commonParams.Linux,
|
|
OSArch: commonParams.Amd64,
|
|
Tags: []string{"linux", "amd64"},
|
|
RunnerBootstrapTimeout: 10,
|
|
TemplateID: &template.ID,
|
|
})
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to create test pool: %s", err))
|
|
}
|
|
|
|
// Create test instance
|
|
instance, err := db.CreateInstance(s.adminCtx, pool.ID, params.CreateInstanceParams{
|
|
Name: "test-instance",
|
|
OSType: commonParams.Linux,
|
|
OSArch: commonParams.Amd64,
|
|
})
|
|
if err != nil {
|
|
s.FailNow(fmt.Sprintf("failed to create test instance: %s", err))
|
|
}
|
|
|
|
providerMock := runnerCommonMocks.NewProvider(s.T())
|
|
poolMgrMock := runnerCommonMocks.NewPoolManager(s.T())
|
|
poolMgrCtrlMock := runnerMocks.NewPoolManagerController(s.T())
|
|
|
|
fixtures := &MetadataTestFixtures{
|
|
AdminContext: s.adminCtx,
|
|
Store: db,
|
|
Providers: map[string]common.Provider{
|
|
"test-provider": providerMock,
|
|
},
|
|
ProviderMock: providerMock,
|
|
PoolMgrMock: poolMgrMock,
|
|
PoolMgrCtrlMock: poolMgrCtrlMock,
|
|
TestInstance: instance,
|
|
TestEntity: entity,
|
|
TestPool: pool,
|
|
TestTemplate: template,
|
|
}
|
|
|
|
s.Fixtures = fixtures
|
|
|
|
// setup test runner
|
|
runner := &Runner{
|
|
providers: fixtures.Providers,
|
|
ctx: fixtures.AdminContext,
|
|
store: fixtures.Store,
|
|
poolManagerCtrl: fixtures.PoolMgrCtrlMock,
|
|
}
|
|
s.Runner = runner
|
|
|
|
// Set up various contexts for testing
|
|
s.setupContexts()
|
|
}
|
|
|
|
func (s *MetadataTestSuite) setupContexts() {
|
|
// Valid instance context
|
|
s.instanceCtx = auth.SetInstanceParams(context.Background(), s.Fixtures.TestInstance)
|
|
s.instanceCtx = auth.SetInstanceRunnerStatus(s.instanceCtx, params.RunnerPending)
|
|
s.instanceCtx = auth.SetInstanceEntity(s.instanceCtx, s.Fixtures.TestEntity)
|
|
s.instanceCtx = auth.SetInstanceAuthToken(s.instanceCtx, "test-auth-token")
|
|
|
|
// Unauthorized context (no instance params)
|
|
s.unauthorizedCtx = context.Background()
|
|
|
|
// Invalid instance context (wrong status)
|
|
s.invalidInstanceCtx = auth.SetInstanceParams(context.Background(), s.Fixtures.TestInstance)
|
|
s.invalidInstanceCtx = auth.SetInstanceRunnerStatus(s.invalidInstanceCtx, params.RunnerActive)
|
|
s.invalidInstanceCtx = auth.SetInstanceEntity(s.invalidInstanceCtx, s.Fixtures.TestEntity)
|
|
|
|
// JIT instance context
|
|
jitInstance := s.Fixtures.TestInstance
|
|
jitInstance.JitConfiguration = map[string]string{
|
|
".runner": base64.StdEncoding.EncodeToString([]byte("runner config")),
|
|
".credentials": base64.StdEncoding.EncodeToString([]byte("credentials config")),
|
|
}
|
|
s.jitInstanceCtx = auth.SetInstanceParams(context.Background(), jitInstance)
|
|
s.jitInstanceCtx = auth.SetInstanceRunnerStatus(s.jitInstanceCtx, params.RunnerPending)
|
|
s.jitInstanceCtx = auth.SetInstanceEntity(s.jitInstanceCtx, s.Fixtures.TestEntity)
|
|
s.jitInstanceCtx = auth.SetInstanceHasJITConfig(s.jitInstanceCtx, jitInstance.JitConfiguration)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetServiceNameForEntity() {
|
|
tests := []struct {
|
|
name string
|
|
entity params.ForgeEntity
|
|
expected string
|
|
hasError bool
|
|
}{
|
|
{
|
|
name: "Organization entity",
|
|
entity: params.ForgeEntity{
|
|
EntityType: params.ForgeEntityTypeOrganization,
|
|
Owner: "test-owner",
|
|
Name: "test-name",
|
|
},
|
|
expected: "actions.runner.test-owner.test-name",
|
|
hasError: false,
|
|
},
|
|
{
|
|
name: "Repository entity",
|
|
entity: params.ForgeEntity{
|
|
EntityType: params.ForgeEntityTypeRepository,
|
|
Owner: "test-owner",
|
|
Name: "test-repo",
|
|
},
|
|
expected: "actions.runner.test-owner-test-repo.test-repo",
|
|
hasError: false,
|
|
},
|
|
{
|
|
name: "Enterprise entity",
|
|
entity: params.ForgeEntity{
|
|
EntityType: params.ForgeEntityTypeEnterprise,
|
|
Owner: "test-enterprise",
|
|
Name: "test-name",
|
|
},
|
|
expected: "actions.runner.test-enterprise.test-name",
|
|
hasError: false,
|
|
},
|
|
{
|
|
name: "Unknown entity type",
|
|
entity: params.ForgeEntity{
|
|
EntityType: "unknown",
|
|
Owner: "test-owner",
|
|
Name: "test-name",
|
|
},
|
|
expected: "",
|
|
hasError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
serviceName, err := s.Runner.getServiceNameForEntity(tt.entity)
|
|
if tt.hasError {
|
|
s.Require().NotNil(err)
|
|
s.Require().Contains(err.Error(), "unknown entity type")
|
|
} else {
|
|
s.Require().Nil(err)
|
|
s.Require().Equal(tt.expected, serviceName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRunnerServiceName() {
|
|
serviceName, err := s.Runner.GetRunnerServiceName(s.instanceCtx)
|
|
|
|
s.Require().Nil(err)
|
|
s.Require().Equal("actions.runner.test-org.test-org", serviceName)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRunnerServiceNameUnauthorized() {
|
|
_, err := s.Runner.GetRunnerServiceName(s.unauthorizedCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetLabelsForInstance() {
|
|
// Test with regular instance
|
|
// Since we can't easily set up the cache in tests, labels might be empty
|
|
// but the function should not panic
|
|
labels := getLabelsForInstance(s.Fixtures.TestInstance)
|
|
s.Require().NotNil(labels) // Should return a slice, even if empty
|
|
|
|
// Test with JIT instance (should return empty labels)
|
|
jitInstance := s.Fixtures.TestInstance
|
|
jitInstance.JitConfiguration = map[string]string{"test": "config"}
|
|
jitLabels := getLabelsForInstance(jitInstance)
|
|
s.Require().Empty(jitLabels)
|
|
|
|
// Test with scale set instance (should return empty labels)
|
|
scaleSetInstance := s.Fixtures.TestInstance
|
|
scaleSetInstance.ScaleSetID = 123
|
|
scaleSetLabels := getLabelsForInstance(scaleSetInstance)
|
|
s.Require().Empty(scaleSetLabels)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRunnerInstallScript() {
|
|
// This test requires complex cache setup for github tools
|
|
// Skipping for now as it would require significant test infrastructure
|
|
s.T().Skip("Skipping install script test - requires github tools cache setup")
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRunnerInstallScriptUnauthorized() {
|
|
_, err := s.Runner.GetRunnerInstallScript(s.unauthorizedCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRunnerInstallScriptInvalidState() {
|
|
_, err := s.Runner.GetRunnerInstallScript(s.invalidInstanceCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGenerateSystemdUnitFile() {
|
|
tests := []struct {
|
|
name string
|
|
runAsUser string
|
|
forgeType params.EndpointType
|
|
expectedTemplate string
|
|
}{
|
|
{
|
|
name: "GitHub with custom user",
|
|
runAsUser: "custom-user",
|
|
forgeType: params.GithubEndpointType,
|
|
expectedTemplate: "GitHub Actions Runner",
|
|
},
|
|
{
|
|
name: "GitHub with default user",
|
|
runAsUser: "",
|
|
forgeType: params.GithubEndpointType,
|
|
expectedTemplate: "GitHub Actions Runner",
|
|
},
|
|
{
|
|
name: "Gitea with custom user",
|
|
runAsUser: "gitea-user",
|
|
forgeType: params.GiteaEndpointType,
|
|
expectedTemplate: "Act Runner",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
// Set up entity with specific forge type
|
|
entity := s.Fixtures.TestEntity
|
|
entity.Credentials.ForgeType = tt.forgeType
|
|
ctx := auth.SetInstanceEntity(context.Background(), entity)
|
|
|
|
unitFile, err := s.Runner.GenerateSystemdUnitFile(ctx, tt.runAsUser)
|
|
|
|
s.Require().Nil(err)
|
|
s.Require().NotEmpty(unitFile)
|
|
s.Require().Contains(string(unitFile), tt.expectedTemplate)
|
|
s.Require().Contains(string(unitFile), "test-org")
|
|
|
|
if tt.runAsUser != "" {
|
|
s.Require().Contains(string(unitFile), tt.runAsUser)
|
|
} else {
|
|
s.Require().Contains(string(unitFile), "runner") // default user
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGenerateSystemdUnitFileUnknownForgeType() {
|
|
entity := s.Fixtures.TestEntity
|
|
entity.Credentials.ForgeType = "unknown"
|
|
ctx := auth.SetInstanceEntity(context.Background(), entity)
|
|
|
|
_, err := s.Runner.GenerateSystemdUnitFile(ctx, "test-user")
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().Contains(err.Error(), "unknown forge type")
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGenerateSystemdUnitFileUnauthorized() {
|
|
_, err := s.Runner.GenerateSystemdUnitFile(s.unauthorizedCtx, "test-user")
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetJITConfigFile() {
|
|
fileName := ".runner"
|
|
expectedContent := "runner config"
|
|
|
|
content, err := s.Runner.GetJITConfigFile(s.jitInstanceCtx, fileName)
|
|
|
|
s.Require().Nil(err)
|
|
s.Require().Equal(expectedContent, string(content))
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetJITConfigFileNotJIT() {
|
|
_, err := s.Runner.GetJITConfigFile(s.instanceCtx, ".runner")
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().Contains(err.Error(), "instance not configured for JIT")
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetJITConfigFileUnauthorized() {
|
|
_, err := s.Runner.GetJITConfigFile(s.unauthorizedCtx, ".runner")
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().Contains(err.Error(), "instance not configured for JIT")
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetJITConfigFileNotFound() {
|
|
_, err := s.Runner.GetJITConfigFile(s.jitInstanceCtx, "nonexistent-file")
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().Contains(err.Error(), "could not find file")
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetInstanceGithubRegistrationToken() {
|
|
expectedToken := "test-registration-token"
|
|
|
|
// Set up mocks
|
|
s.Fixtures.PoolMgrCtrlMock.On("GetOrgPoolManager", mock.AnythingOfType("params.Organization")).Return(s.Fixtures.PoolMgrMock, nil)
|
|
s.Fixtures.PoolMgrMock.On("GithubRunnerRegistrationToken").Return(expectedToken, nil)
|
|
|
|
token, err := s.Runner.GetInstanceGithubRegistrationToken(s.instanceCtx)
|
|
|
|
s.Require().Nil(err)
|
|
s.Require().Equal(expectedToken, token)
|
|
|
|
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
|
|
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetInstanceGithubRegistrationTokenUnauthorized() {
|
|
_, err := s.Runner.GetInstanceGithubRegistrationToken(s.unauthorizedCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetInstanceGithubRegistrationTokenInvalidState() {
|
|
_, err := s.Runner.GetInstanceGithubRegistrationToken(s.invalidInstanceCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetInstanceGithubRegistrationTokenAlreadyFetched() {
|
|
// Set up context with token already fetched
|
|
ctx := auth.SetInstanceTokenFetched(s.instanceCtx, true)
|
|
|
|
_, err := s.Runner.GetInstanceGithubRegistrationToken(ctx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetInstanceGithubRegistrationTokenJITConfig() {
|
|
_, err := s.Runner.GetInstanceGithubRegistrationToken(s.jitInstanceCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRootCertificateBundle() {
|
|
expectedBundle := params.CertificateBundle{
|
|
RootCertificates: map[string][]byte{
|
|
"test-ca": []byte("test-certificate"),
|
|
},
|
|
}
|
|
|
|
// Set up mocks
|
|
s.Fixtures.PoolMgrCtrlMock.On("GetOrgPoolManager", mock.AnythingOfType("params.Organization")).Return(s.Fixtures.PoolMgrMock, nil)
|
|
s.Fixtures.PoolMgrMock.On("RootCABundle").Return(expectedBundle, nil)
|
|
|
|
bundle, err := s.Runner.GetRootCertificateBundle(s.instanceCtx)
|
|
|
|
s.Require().Nil(err)
|
|
s.Require().Equal(expectedBundle.RootCertificates, bundle.RootCertificates)
|
|
|
|
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
|
|
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRootCertificateBundleUnauthorized() {
|
|
_, err := s.Runner.GetRootCertificateBundle(s.unauthorizedCtx)
|
|
|
|
s.Require().NotNil(err)
|
|
s.Require().ErrorIs(err, runnerErrors.ErrUnauthorized)
|
|
}
|
|
|
|
func (s *MetadataTestSuite) TestGetRootCertificateBundleInvalidBundle() {
|
|
// Set up mocks to return error for invalid bundle
|
|
s.Fixtures.PoolMgrCtrlMock.On("GetOrgPoolManager", mock.AnythingOfType("params.Organization")).Return(s.Fixtures.PoolMgrMock, nil)
|
|
s.Fixtures.PoolMgrMock.On("RootCABundle").Return(params.CertificateBundle{}, fmt.Errorf("invalid bundle"))
|
|
s.Fixtures.PoolMgrMock.On("ID").Return("test-pool-manager-id")
|
|
|
|
bundle, err := s.Runner.GetRootCertificateBundle(s.instanceCtx)
|
|
|
|
// Should return empty bundle without error when CA bundle is invalid
|
|
s.Require().Nil(err)
|
|
s.Require().Empty(bundle.RootCertificates)
|
|
|
|
s.Fixtures.PoolMgrMock.AssertExpectations(s.T())
|
|
s.Fixtures.PoolMgrCtrlMock.AssertExpectations(s.T())
|
|
}
|
|
|
|
func TestMetadataTestSuite(t *testing.T) {
|
|
suite.Run(t, new(MetadataTestSuite))
|
|
}
|