Files
ttstd-infinite-canvas/backend/internal/app/admin_channel_test.go
T

548 lines
21 KiB
Go

package app
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestChannelFromRequestStoresConnectionWithoutDefaultProtocol(t *testing.T) {
channel, err := channelFromRequest(ChannelRequest{
Name: "混合模型渠道",
BaseURL: "https://8.8.8.8/v1",
APIKey: "access-key",
SecretKey: "secret-key",
ConcurrencyLimit: intPtr(6),
Models: []string{"seedance-2.0"},
}, model.ModelChannel{})
if err != nil {
t.Fatalf("channelFromRequest() error = %v", err)
}
if channel.APIFormat != "openai" {
t.Fatalf("APIFormat = %q, want openai", channel.APIFormat)
}
if channel.ConcurrencyLimit != 6 {
t.Fatalf("ConcurrencyLimit = %d, want 6", channel.ConcurrencyLimit)
}
if channel.APIKey != "access-key" || channel.SecretKey != "secret-key" {
t.Fatal("channel credentials were not stored")
}
}
func TestMergeChannelRequestSupportsEnabledOnlyPatch(t *testing.T) {
enabled := false
req := mergeChannelRequest(ChannelRequest{Enabled: &enabled}, model.ModelChannel{
Name: "Video",
BaseURL: "https://example.com/v1",
APIFormat: "openai",
ModelsJSON: `["custom-video"]`,
HeadersJSON: `[{"name":"User-Agent","value":"Stored Agent"}]`,
})
if req.Name != "Video" || req.BaseURL != "https://example.com/v1" || len(req.Models) != 1 || len(req.Headers) != 1 {
t.Fatalf("mergeChannelRequest() = %#v", req)
}
}
func TestUpdateSystemChannelEnabledOnlySkipsOutboundResolution(t *testing.T) {
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Dead", BaseURL: "https://dead.invalid/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
disabled := false
updated, err := svc.UpdateSystemChannel(admin, channel.ID, ChannelRequest{Enabled: &disabled})
if err != nil {
t.Fatalf("UpdateSystemChannel() should not resolve the stored URL for an enabled-only patch: %v", err)
}
if updated.Enabled {
t.Fatal("UpdateSystemChannel() did not persist disabled state")
}
var stored model.ModelChannel
if err := db.First(&stored, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Enabled {
t.Fatal("stored channel is still enabled after update")
}
}
func TestDuplicateSystemChannelCopiesSecretsModelsAndPriceTiers(t *testing.T) {
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
source := model.ModelChannel{
ID: "channel-source",
UserID: admin.ID,
Scope: model.ChannelScopeSystem,
Enabled: false,
Name: "方舟视频",
PublicAlias: "视频主链路",
BaseURL: "https://example.com/v1",
APIKey: "source-key",
SecretKey: "source-secret",
APIFormat: "openai",
ConcurrencyLimit: 6,
ModelsJSON: `["seedance-2"]`,
HeadersJSON: `[{"name":"X-Tenant","value":"tenant-a"}]`,
}
if err := svc.encryptSystemChannelSecrets(&source); err != nil {
t.Fatal(err)
}
if err := db.Create(&source).Error; err != nil {
t.Fatal(err)
}
sourceModel := model.ChannelModel{
ID: "model-source",
ChannelID: source.ID,
ModelKey: "seedance-2",
ProviderModelKey: "seedance-2",
DisplayName: "Seedance 2",
SortOrder: 3,
Capability: "video",
Protocol: model.ChannelInterfaceVolcengineArkVideo,
BillingMode: "fixed_request",
UnitPriceMicrocredits: 120,
PriceConfigured: true,
Enabled: true,
PriceVersion: 4,
}
if err := db.Create(&sourceModel).Error; err != nil {
t.Fatal(err)
}
sourceTier := model.ChannelModelPriceTier{
ID: "tier-source",
ChannelModelID: sourceModel.ID,
SelectorKey: `{}`,
SelectorJSON: `{}`,
Resolution: "720p",
ProviderModelKey: "seedance-2",
BillingMode: "fixed_request",
UnitPriceMicrocredits: 120,
PriceConfigured: true,
Enabled: true,
}
if err := db.Create(&sourceTier).Error; err != nil {
t.Fatal(err)
}
copied, err := svc.DuplicateSystemChannel(admin, source.ID)
if err != nil {
t.Fatalf("DuplicateSystemChannel() error = %v", err)
}
if copied.Name != "方舟视频 - 副本" || copied.Enabled != source.Enabled {
t.Fatalf("copied channel summary = %#v", copied)
}
var stored model.ModelChannel
if err := db.First(&stored, "id = ?", copied.ID).Error; err != nil {
t.Fatal(err)
}
if err := svc.decryptSystemChannelSecrets(&stored); err != nil {
t.Fatal(err)
}
if stored.APIKey != "source-key" || stored.SecretKey != "source-secret" || stored.HeadersJSON != source.HeadersJSON {
t.Fatalf("copied channel connection = %#v", stored)
}
models, err := svc.repo.ChannelModels(copied.ID, true)
if err != nil {
t.Fatal(err)
}
if len(models) != 1 || models[0].ID == sourceModel.ID || models[0].ModelKey != sourceModel.ModelKey || models[0].SortOrder != sourceModel.SortOrder {
t.Fatalf("copied models = %#v", models)
}
if len(models[0].PriceTiers) != 1 || models[0].PriceTiers[0].ID == sourceTier.ID || models[0].PriceTiers[0].UnitPriceMicrocredits != sourceTier.UnitPriceMicrocredits {
t.Fatalf("copied price tiers = %#v", models[0].PriceTiers)
}
}
func TestChannelFromRequestStoresAndClearsHeaders(t *testing.T) {
// URL validation is real, but this non-network test must not depend on DNS.
request := ChannelRequest{Name: "Headers", BaseURL: "https://93.184.216.34/v1", Headers: []OutboundHeader{{Name: "User-Agent", Value: "Custom Agent"}}}
channel, err := channelFromRequest(request, model.ModelChannel{})
if err != nil {
t.Fatal(err)
}
if channel.HeadersJSON != `[{"name":"User-Agent","value":"Custom Agent"}]` {
t.Fatalf("HeadersJSON = %q", channel.HeadersJSON)
}
request.Headers = []OutboundHeader{}
channel, err = channelFromRequest(request, channel)
if err != nil {
t.Fatal(err)
}
if channel.HeadersJSON != `[]` {
t.Fatalf("cleared HeadersJSON = %q", channel.HeadersJSON)
}
}
func TestPublicChannelOnlyReturnsSystemHeadersToAdmin(t *testing.T) {
channel := model.ModelChannel{ID: "system-1", Scope: model.ChannelScopeSystem, BaseURL: "https://example.com/v1", HeadersJSON: `[{"name":"X-Gateway-Tenant","value":"tenant-a"}]`}
adminView := publicChannel(channel, true, nil)
if len(adminView.Headers) != 1 || adminView.Headers[0].Name != "X-Gateway-Tenant" {
t.Fatalf("admin headers = %#v", adminView.Headers)
}
userView := publicChannel(channel, false, nil)
if len(userView.Headers) != 0 {
t.Fatalf("user headers = %#v", userView.Headers)
}
}
func TestChannelFromRequestRejectsInvalidConcurrencyLimit(t *testing.T) {
for _, limit := range []int{0, 1000} {
_, err := channelFromRequest(ChannelRequest{Name: "Bad", BaseURL: "https://example.com/v1", ConcurrencyLimit: &limit}, model.ModelChannel{})
if err == nil {
t.Fatalf("channelFromRequest() concurrencyLimit = %d, error = nil", limit)
}
}
}
func TestRuntimeConcurrencyUsesEnvironmentFallback(t *testing.T) {
t.Setenv("CANVAS_CHANNEL_CONCURRENCY", "7")
t.Setenv("CANVAS_WORKER_CONCURRENCY", "9")
setting := defaultRuntimePolicy().Task
if setting.ChannelConcurrency != 7 || setting.WorkerConcurrency != 9 {
t.Fatalf("runtimeConcurrencyFromEnvironment() = %#v", setting)
}
useGlobal := true
channel, err := channelFromRequest(ChannelRequest{Name: "Global", BaseURL: "https://93.184.216.34/v1", UseGlobalConcurrency: &useGlobal}, model.ModelChannel{ConcurrencyLimit: 4})
if err != nil || channel.ConcurrencyLimit != 0 {
t.Fatalf("global concurrency channel = %#v, error = %v", channel, err)
}
}
func TestFetchAdminChannelModelsReaddsDeletedModel(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
deleted := model.ChannelModel{ID: "deleted-model", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "model-a", BillingMode: "fixed_request", PriceVersion: 1}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&deleted).Error; err != nil {
t.Fatal(err)
}
if err := db.Delete(&deleted).Error; err != nil {
t.Fatal(err)
}
result, err := svc.FetchAdminChannelModels(context.Background(), admin, channel.ID)
if err != nil {
t.Fatal(err)
}
if result.Added != 1 {
t.Fatalf("Added = %d, want 1", result.Added)
}
var active model.ChannelModel
if err := db.First(&active, "channel_id = ? AND model_key = ?", channel.ID, "model-a").Error; err != nil {
t.Fatal(err)
}
if active.ID == deleted.ID || active.Enabled || active.PriceConfigured {
t.Fatalf("re-added model = %#v", active)
}
var total int64
if err := db.Unscoped().Model(&model.ChannelModel{}).Where("channel_id = ? AND model_key = ?", channel.ID, "model-a").Count(&total).Error; err != nil {
t.Fatal(err)
}
if total != 2 {
t.Fatalf("model history count = %d, want 2", total)
}
}
func TestImportAdminChannelModelsOnlyImportsSelectedModels(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"},{"id":"model-b"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
preview, err := svc.PreviewAdminChannelModels(context.Background(), admin, channel.ID)
if err != nil {
t.Fatal(err)
}
if len(preview) != 2 {
t.Fatalf("preview models = %#v, want two models", preview)
}
var before int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&before).Error; err != nil {
t.Fatal(err)
}
if before != 0 {
t.Fatalf("preview created %d channel models", before)
}
result, err := svc.ImportAdminChannelModels(context.Background(), admin, channel.ID, []string{"model-b"})
if err != nil {
t.Fatal(err)
}
if result.Added != 1 || len(result.Models) != 1 || result.Models[0] != "model-b" {
t.Fatalf("import result = %#v, want only model-b", result)
}
var imported []model.ChannelModel
if err := db.Where("channel_id = ?", channel.ID).Find(&imported).Error; err != nil {
t.Fatal(err)
}
if len(imported) != 1 || imported[0].ModelKey != "model-b" {
t.Fatalf("imported models = %#v, want only model-b", imported)
}
}
func TestImportAdminChannelModelsRejectsUnknownSelection(t *testing.T) {
t.Setenv("CANVAS_ALLOWED_PRIVATE_UPSTREAM_HOSTS", "127.0.0.1")
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{"id":"model-a"}]}`))
}))
defer upstream.Close()
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: upstream.URL + "/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
_, err := svc.ImportAdminChannelModels(context.Background(), admin, channel.ID, []string{"not-in-catalog"})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Message != "所选模型不在上游模型目录中:not-in-catalog" {
t.Fatalf("ImportAdminChannelModels() error = %#v", err)
}
var count int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("unknown selection created %d channel models", count)
}
}
func TestSaveAdminChannelModelRejectsActiveDuplicateKey(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `[]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Capability: "text", Protocol: model.ChannelInterfaceChatCompletion, BillingMode: "fixed_request", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Capability: "text", Protocol: model.ChannelInterfaceChatCompletion, BillingMode: "fixed_request", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
enabled := true
_, err := svc.SaveAdminChannelModel(admin, channel.ID, items[0].ID, ChannelModelRequest{ModelKey: "model-b", DisplayName: "Duplicate", Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion), BillingMode: "fixed_request", Enabled: &enabled})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Status != http.StatusBadRequest || authErr.Message != "该渠道已存在模型 model-b,请直接编辑已有模型" {
t.Fatalf("SaveAdminChannelModel() error = %#v", err)
}
}
func TestDeleteAdminChannelModelsDeletesSelectionAtomically(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `["model-a","model-b","model-c"]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Enabled: true, PriceVersion: 1},
{ID: "model-c", ChannelID: channel.ID, ModelKey: "model-c", DisplayName: "Model C", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
deleted, err := svc.DeleteAdminChannelModels(admin, channel.ID, []string{" model-a ", "model-b", "model-a"})
if err != nil {
t.Fatal(err)
}
if deleted != 2 {
t.Fatalf("deleted = %d, want 2", deleted)
}
var storedChannel model.ModelChannel
if err := db.First(&storedChannel, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if storedChannel.ModelsJSON != `["model-c"]` {
t.Fatalf("ModelsJSON = %s, want model-c only", storedChannel.ModelsJSON)
}
var active []model.ChannelModel
if err := db.Where("channel_id = ?", channel.ID).Find(&active).Error; err != nil {
t.Fatal(err)
}
if len(active) != 1 || active[0].ID != "model-c" {
t.Fatalf("active models = %#v, want model-c", active)
}
var removed []model.ChannelModel
if err := db.Unscoped().Where("channel_id = ? AND id IN ?", channel.ID, []string{"model-a", "model-b"}).Find(&removed).Error; err != nil {
t.Fatal(err)
}
if len(removed) != 2 {
t.Fatalf("removed models = %#v, want two", removed)
}
for _, item := range removed {
if item.Enabled || item.PriceVersion != 2 || !item.DeletedAt.Valid {
t.Fatalf("removed model state = %#v", item)
}
}
}
func TestDeleteAdminChannelModelsRejectsWholeSelectionWhenOneModelIsInUse(t *testing.T) {
svc, db := newChannelModelTestService(t)
admin := &model.User{ID: "admin", Role: model.UserRoleAdmin}
channel := model.ModelChannel{ID: "channel-1", UserID: admin.ID, Scope: model.ChannelScopeSystem, Enabled: true, Name: "Test", BaseURL: "https://example.com/v1", APIKey: "key", APIFormat: "openai", ModelsJSON: `["model-a","model-b"]`}
items := []model.ChannelModel{
{ID: "model-a", ChannelID: channel.ID, ModelKey: "model-a", DisplayName: "Model A", Enabled: true, PriceVersion: 1},
{ID: "model-b", ChannelID: channel.ID, ModelKey: "model-b", DisplayName: "Model B", Enabled: true, PriceVersion: 1},
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&items).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&model.Task{ID: "task-running", ChannelModelID: "model-b", Status: model.TaskStatusRunning}).Error; err != nil {
t.Fatal(err)
}
deleted, err := svc.DeleteAdminChannelModels(admin, channel.ID, []string{"model-a", "model-b"})
var authErr *AuthError
if !errors.As(err, &authErr) || authErr.Message != "所选渠道模型中有模型仍被前台模型供应线路或进行中任务使用,本次未删除任何模型" {
t.Fatalf("DeleteAdminChannelModels() deleted = %d, error = %#v", deleted, err)
}
var active int64
if err := db.Model(&model.ChannelModel{}).Where("channel_id = ?", channel.ID).Count(&active).Error; err != nil {
t.Fatal(err)
}
if active != 2 {
t.Fatalf("active models = %d, want 2 after atomic rejection", active)
}
var storedChannel model.ModelChannel
if err := db.First(&storedChannel, "id = ?", channel.ID).Error; err != nil {
t.Fatal(err)
}
if storedChannel.ModelsJSON != channel.ModelsJSON {
t.Fatalf("ModelsJSON changed after rejected batch: %s", storedChannel.ModelsJSON)
}
}
func TestNormalizeAdminChannelModelDeleteIDsRequiresBoundedSelection(t *testing.T) {
if _, err := normalizeAdminChannelModelDeleteIDs([]string{"", " "}); err == nil {
t.Fatal("empty selection should be rejected")
}
values := make([]string, 101)
for index := range values {
values[index] = "model-" + strconv.Itoa(index)
}
if _, err := normalizeAdminChannelModelDeleteIDs(values); err == nil {
t.Fatal("selection above 100 models should be rejected")
}
}
func TestResolveProviderConfigMapsSKUToProviderModel(t *testing.T) {
svc, db := newChannelModelTestService(t)
svc.dataDir = t.TempDir()
channel := model.ModelChannel{
ID: "channel-1", Scope: model.ChannelScopeSystem, Enabled: true, Name: "Seedance",
BaseURL: "https://ark.cn-beijing.volces.com/api/v3", APIKey: "test-key", APIFormat: "openai", ModelsJSON: `["seedance-2-5-480p"]`,
}
if err := svc.encryptSystemChannelSecrets(&channel); err != nil {
t.Fatal(err)
}
item := model.ChannelModel{
ID: "model-1", ChannelID: channel.ID, ModelKey: "seedance-2-5-480p", ProviderModelKey: "doubao-seedance-2-5",
Capability: "video", Protocol: model.ChannelInterfaceVolcengineArkVideo, Enabled: true,
}
if err := db.Create(&channel).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&item).Error; err != nil {
t.Fatal(err)
}
config, err := svc.resolveProviderConfig(providerConfig{ChannelID: channel.ID, Model: item.ModelKey})
if err != nil {
t.Fatal(err)
}
if config.ChannelModelKey != item.ModelKey || config.Model != item.ProviderModelKey {
t.Fatalf("resolved config = %#v", config)
}
}
func TestValidateTaskCapabilityFixesSingleResolutionSKU(t *testing.T) {
svc, db := newChannelModelTestService(t)
video := DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceVolcengineArkVideo), "doubao-seedance-2-5").Video
video.References.MaxVideos = 0
video.Resolutions = []string{"480p"}
video.DefaultResolution = "480p"
encoded, err := json.Marshal(&ModelCapabilityConfig{Version: 1, Video: video})
if err != nil {
t.Fatal(err)
}
channelModel := model.ChannelModel{
ID: "model-480p", ChannelID: "channel-1", ModelKey: "doubao-seedance-2-5-480p", ProviderModelKey: "doubao-seedance-2-5",
Capability: "video", Protocol: model.ChannelInterfaceVolcengineArkVideo, Enabled: true, CapabilityConfigJSON: string(encoded),
}
if err := db.Create(&channelModel).Error; err != nil {
t.Fatal(err)
}
input := map[string]any{
"mode": "video",
"config": map[string]any{
"channelId": "channel-1", "model": channelModel.ModelKey, "vquality": "auto", "videoSeconds": "6", "size": "16:9",
"videoGenerateAudio": "true", "videoWatermark": "false",
},
}
if err := svc.ValidateTaskCapability(input); err != nil {
t.Fatal(err)
}
config := input["config"].(map[string]any)
if got := config["vquality"]; got != "480p" {
t.Fatalf("vquality = %#v, want 480p", got)
}
}
func newChannelModelTestService(t *testing.T) (*Service, *gorm.DB) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+newID()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.ModelChannel{}, &model.ChannelModel{}, &model.ChannelModelPriceTier{}, &model.LogicalModel{}, &model.LogicalModelRevision{}, &model.LogicalModelRoute{}, &model.Task{}, &model.IDSequence{}); err != nil {
t.Fatal(err)
}
return &Service{repo: repository.New(db)}, db
}
func intPtr(value int) *int { return &value }