mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
* feat(i18n): translate web console messages in the frontend and AI-client errors in the backend
Chinese text was hard-coded in API messages, stored log content, emails and
server logs, so it reached users of every interface language.
Text is now translated where its reader is:
- Web console APIs return the English source text as message_key with
message_params, and the web console translates it in all seven languages.
message keeps the English text for other API callers.
- Errors returned to AI clients, emails and notifications are translated by
the backend (en, zh-CN, zh-TW) in the language saved on the account, then
the language of the request's Accept-Language header.
- A reader who states no language still gets the original Chinese text of the
messages that were Chinese before, word for word. Gateways that use another
new-api as their upstream match that text, for example to disable a channel
whose account ran out of quota.
- Usage, top-up and system log content is stored as content_parts (key and
params) and rendered in the viewer's language. Task fail reasons and refund
reasons written by the gateway are English keys. Rows written earlier are
shown as before.
- Server logs are English.
Also:
- The saved interface language zhTW, and zh-HK, zh-MO and zh-Hant, now select
Traditional Chinese in the backend instead of Simplified Chinese.
- The built-in group descriptions are translated; descriptions an
administrator saved are shown as written.
- .agents/rules/i18n.md records the conventions. Tests in i18n/ fail on a
non-English string literal, on a web console message without translations
and on a locale key without a constant.
* feat(i18n): print server logs in Chinese when LOG_LANGUAGE asks for it
Server log lines are English by default. With LOG_LANGUAGE=zh-CN, the lines
that were Chinese before print their original Chinese text.
common.LogText formats a log line from its English format, which is also the
key of its Chinese text in common/log_text.zh-CN.json. A line without an entry
prints in English. go vet checks the arguments of every call, and
TestServerLogTexts checks that each entry still matches a call and has the
same verbs as its key.
* docs: list LOG_LANGUAGE in the docker compose example
* feat(i18n): translate every server log line and add DEFAULT_LANGUAGE
Every line the gateway prints goes through common.LogText. With
DEFAULT_LANGUAGE=zh-CN, the lines that have a Chinese text in
common/log_text.zh-CN.json print it and the others print English.
DEFAULT_LANGUAGE replaces LOG_LANGUAGE. It also sets the language of
backend messages, emails and notifications for a reader who states no
language: en, zh-CN or zh-TW, English when unset. The list of messages
that stayed Chinese for such readers is removed.
Review fixes:
- Channel test logs store the token name "Model test"; the console shows
it in the interface language.
- Task fail reasons written by the gateway are fixed sentences the console
translates. The HTTP status and the upstream detail go to the server log
when a polled task is failed.
- The auto-disable reason of a slow channel is written in DEFAULT_LANGUAGE.
Also:
- Drop the repository-scanning lint tests, the i18n rules file and its
read gate. AGENTS.md keeps the conventions in four lines.
- Payment error toasts keep their previous text and only add the
translated message key.
- Remove guards and helpers that no caller needs.
* fix(i18n): address review on language lookup, notices and error responses
- Relay requests no longer load the user a second time to find the
language: the settings TokenAuth put into the request are used as they
are, also when they hold no language.
- Upstream model update notices are rendered in each recipient's saved
language, DEFAULT_LANGUAGE when none.
- /api/usage/token and /api/log/token answer with backend-translated
messages again, with the keys and status codes used before this PR.
Their callers are scripts, not the web console.
- Webhook notifications fill {{value}} placeholders like the other
senders. Running the rendered text through Sprintf appended
%!(EXTRA ...) and broke %2F in links.
- RootUserLanguage reads the setting of the root user it already loaded.
- zh_TW and zh_HK are read as Traditional Chinese.
- Add common.ApiErrorStatus for error responses under another HTTP status
and use it where those responses were built by hand.
- Remove middleware.GetLanguage, which had no callers.
496 lines
16 KiB
Go
496 lines
16 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/pkg/billingexpr"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/QuantumNous/new-api/types"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetChannelDefaultBaseURLsUsesBuiltInDefaults(t *testing.T) {
|
|
originalBaseURLs := constant.ChannelBaseURLs
|
|
constant.ChannelBaseURLs = append([]string(nil), originalBaseURLs...)
|
|
constant.ChannelBaseURLs[constant.ChannelTypeDeepSeek] = "https://deepseek.server.example"
|
|
t.Cleanup(func() {
|
|
constant.ChannelBaseURLs = originalBaseURLs
|
|
})
|
|
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/channel/default_base_urls", nil)
|
|
GetChannelDefaultBaseURLs(c)
|
|
|
|
require.Equal(t, http.StatusOK, recorder.Code)
|
|
var response struct {
|
|
Success bool `json:"success"`
|
|
Data map[int]string `json:"data"`
|
|
}
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
|
|
require.True(t, response.Success)
|
|
assert.Equal(t, "https://deepseek.server.example", response.Data[constant.ChannelTypeDeepSeek])
|
|
assert.Equal(t, "https://api.openai.com", response.Data[constant.ChannelTypeOpenAI])
|
|
assert.NotContains(t, response.Data, constant.ChannelTypeAzure)
|
|
assert.NotContains(t, response.Data, constant.ChannelTypeNewAPI)
|
|
assert.NotContains(t, response.Data, constant.ChannelTypeTaskPlugin)
|
|
}
|
|
|
|
func TestValidateChannelProxy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
proxy string
|
|
wantErr bool
|
|
}{
|
|
{name: "empty"},
|
|
{name: "http", proxy: "http://proxy.example:8080"},
|
|
{name: "https", proxy: "https://proxy.example:8443"},
|
|
{name: "socks5", proxy: "socks5://proxy.example"},
|
|
{name: "socks5h", proxy: "socks5h://proxy.example:1080/"},
|
|
{name: "unsupported", proxy: "ftp://proxy.example", wantErr: true},
|
|
{name: "path", proxy: "socks5://proxy.example:1080/path", wantErr: true},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
setting, err := common.Marshal(dto.ChannelSettings{Proxy: test.proxy})
|
|
require.NoError(t, err)
|
|
channel := &model.Channel{
|
|
Type: constant.ChannelTypeOpenAI,
|
|
Setting: common.GetPointer(string(setting)),
|
|
}
|
|
|
|
err = validateChannel(channel, false)
|
|
|
|
if test.wantErr {
|
|
require.ErrorContains(t, err, "invalid channel proxy")
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateChannelRequiresNewAPIBaseURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
baseURL *string
|
|
wantErr bool
|
|
}{
|
|
{name: "missing", wantErr: true},
|
|
{name: "blank", baseURL: common.GetPointer(" "), wantErr: true},
|
|
{name: "configured", baseURL: common.GetPointer("https://new-api.example")},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
channel := &model.Channel{
|
|
Type: constant.ChannelTypeNewAPI,
|
|
BaseURL: test.baseURL,
|
|
}
|
|
|
|
err := validateChannel(channel, false)
|
|
|
|
if test.wantErr {
|
|
require.ErrorContains(t, err, "New API channel base URL cannot be empty")
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewAPIChannelRegistration(t *testing.T) {
|
|
apiType, ok := common.ChannelType2APIType(constant.ChannelTypeNewAPI)
|
|
|
|
require.True(t, ok)
|
|
assert.Equal(t, constant.APITypeNewAPI, apiType)
|
|
assert.Equal(t, "New API", constant.GetChannelTypeName(constant.ChannelTypeNewAPI))
|
|
require.Greater(t, len(constant.ChannelBaseURLs), constant.ChannelTypeNewAPI)
|
|
assert.Empty(t, constant.ChannelBaseURLs[constant.ChannelTypeNewAPI])
|
|
}
|
|
|
|
func TestResponsesCompactChannelSupport(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
channelType int
|
|
apiType int
|
|
want bool
|
|
}{
|
|
{name: "OpenAI", channelType: constant.ChannelTypeOpenAI, apiType: constant.APITypeOpenAI, want: true},
|
|
{name: "Azure", channelType: constant.ChannelTypeAzure, apiType: constant.APITypeOpenAI, want: true},
|
|
{name: "Codex", channelType: constant.ChannelTypeCodex, apiType: constant.APITypeCodex, want: true},
|
|
{name: "Advanced Custom", channelType: constant.ChannelTypeAdvancedCustom, apiType: constant.APITypeAdvancedCustom, want: true},
|
|
{name: "Sub2API", channelType: constant.ChannelTypeSub2API, apiType: constant.APITypeSub2API, want: true},
|
|
{name: "New API", channelType: constant.ChannelTypeNewAPI, apiType: constant.APITypeNewAPI, want: true},
|
|
{name: "Anthropic", channelType: constant.ChannelTypeAnthropic, apiType: constant.APITypeAnthropic, want: false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert.Equal(t, test.want, common.SupportsResponsesCompact(test.channelType, test.apiType))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMultiprotocolGatewayEndpointTypes(t *testing.T) {
|
|
want := []constant.EndpointType{
|
|
constant.EndpointTypeOpenAI,
|
|
constant.EndpointTypeOpenAIResponse,
|
|
constant.EndpointTypeOpenAIResponseCompact,
|
|
constant.EndpointTypeAnthropic,
|
|
constant.EndpointTypeGemini,
|
|
constant.EndpointTypeOpenAIAlphaSearch,
|
|
}
|
|
|
|
assert.Equal(t, want, common.GetEndpointTypesByChannelType(constant.ChannelTypeNewAPI, "gpt-5"))
|
|
assert.Equal(t, want, common.GetEndpointTypesByChannelType(constant.ChannelTypeSub2API, "gpt-5"))
|
|
}
|
|
|
|
func TestCopyChannelRejectsInvalidLegacyProxySettings(t *testing.T) {
|
|
db := setupModelListControllerTestDB(t)
|
|
settingBytes, err := common.Marshal(dto.ChannelSettings{
|
|
Proxy: "socks5://proxy.example/legacy-path",
|
|
})
|
|
require.NoError(t, err)
|
|
setting := string(settingBytes)
|
|
origin := &model.Channel{
|
|
Type: constant.ChannelTypeOpenAI,
|
|
Name: "legacy proxy channel",
|
|
Key: "test-key",
|
|
Models: "gpt-test",
|
|
Group: "default",
|
|
Setting: &setting,
|
|
}
|
|
require.NoError(t, db.Create(origin).Error)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Params = gin.Params{{Key: "id", Value: fmt.Sprintf("%d", origin.Id)}}
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/channel/copy", nil)
|
|
|
|
CopyChannel(ctx)
|
|
|
|
assert.Contains(t, recorder.Body.String(), "invalid channel settings")
|
|
var channelCount int64
|
|
require.NoError(t, db.Model(&model.Channel{}).Count(&channelCount).Error)
|
|
assert.Equal(t, int64(1), channelCount)
|
|
}
|
|
|
|
func TestDeleteChannelResetsProxyCacheWhenPreReadFails(t *testing.T) {
|
|
db := setupModelListControllerTestDB(t)
|
|
require.NoError(t, db.AutoMigrate(&model.Log{}, &model.AuditLog{}))
|
|
service.ResetProxyClientCache()
|
|
t.Cleanup(service.ResetProxyClientCache)
|
|
|
|
proxyURL := "http://proxy.example:8080"
|
|
beforeDelete, err := service.GetHttpClientWithProxy(proxyURL)
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Params = gin.Params{{Key: "id", Value: "999999"}}
|
|
ctx.Request = httptest.NewRequest(http.MethodDelete, "/api/channel/999999", nil)
|
|
|
|
DeleteChannel(ctx)
|
|
|
|
assert.Contains(t, recorder.Body.String(), `"success":true`)
|
|
afterDelete, err := service.GetHttpClientWithProxy(proxyURL)
|
|
require.NoError(t, err)
|
|
assert.NotSame(t, beforeDelete, afterDelete)
|
|
}
|
|
|
|
func TestDeleteChannelBatchReportsAndAuditsActualDeletedCount(t *testing.T) {
|
|
db := setupModelListControllerTestDB(t)
|
|
require.NoError(t, db.AutoMigrate(&model.Log{}, &model.AuditLog{}))
|
|
channel := &model.Channel{Name: "existing", Key: "test-key"}
|
|
require.NoError(t, db.Create(channel).Error)
|
|
|
|
requestBody, err := common.Marshal(ChannelBatch{Ids: []int{channel.Id, 999999}})
|
|
require.NoError(t, err)
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodDelete, "/api/channel/batch", bytes.NewReader(requestBody))
|
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
DeleteChannelBatch(ctx)
|
|
|
|
var response struct {
|
|
Success bool `json:"success"`
|
|
Data int64 `json:"data"`
|
|
}
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
|
|
assert.True(t, response.Success)
|
|
assert.Equal(t, int64(1), response.Data)
|
|
|
|
var auditLog model.AuditLog
|
|
require.NoError(t, db.Order("id desc").First(&auditLog).Error)
|
|
var auditData struct {
|
|
Operation struct {
|
|
Params map[string]any `json:"params"`
|
|
} `json:"op"`
|
|
}
|
|
encodedAudit, err := common.Marshal(auditLog.Other)
|
|
require.NoError(t, err)
|
|
require.NoError(t, common.Unmarshal(encodedAudit, &auditData))
|
|
assert.Equal(t, float64(1), auditData.Operation.Params["count"])
|
|
}
|
|
|
|
func TestSettleTestQuotaUsesTieredBilling(t *testing.T) {
|
|
info := &relaycommon.RelayInfo{
|
|
TieredBillingSnapshot: &billingexpr.BillingSnapshot{
|
|
BillingMode: "tiered_expr",
|
|
ExprString: `param("stream") == true ? tier("stream", p * 3) : tier("base", p * 2)`,
|
|
ExprHash: billingexpr.ExprHashString(`param("stream") == true ? tier("stream", p * 3) : tier("base", p * 2)`),
|
|
GroupRatio: 1,
|
|
EstimatedTier: "stream",
|
|
QuotaPerUnit: common.QuotaPerUnit,
|
|
ExprVersion: 1,
|
|
},
|
|
BillingRequestInput: &billingexpr.RequestInput{
|
|
Body: []byte(`{"stream":true}`),
|
|
},
|
|
}
|
|
|
|
quota, result := settleTestQuota(info, types.PriceData{
|
|
ModelRatio: 1,
|
|
CompletionRatio: 2,
|
|
}, &dto.Usage{
|
|
PromptTokens: 1000,
|
|
})
|
|
|
|
require.Equal(t, 1500, quota)
|
|
require.NotNil(t, result)
|
|
require.Equal(t, "stream", result.MatchedTier)
|
|
}
|
|
|
|
func TestBuildTestLogOtherInjectsTieredInfo(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
|
|
info := &relaycommon.RelayInfo{
|
|
TieredBillingSnapshot: &billingexpr.BillingSnapshot{
|
|
BillingMode: "tiered_expr",
|
|
ExprString: `tier("base", p * 2)`,
|
|
},
|
|
ChannelMeta: &relaycommon.ChannelMeta{},
|
|
}
|
|
priceData := types.PriceData{
|
|
GroupRatioInfo: types.GroupRatioInfo{GroupRatio: 1},
|
|
}
|
|
usage := &dto.Usage{
|
|
PromptTokensDetails: dto.InputTokenDetails{
|
|
CachedTokens: 12,
|
|
},
|
|
}
|
|
|
|
requestRules := []billingexpr.RequestRuleTrace{{
|
|
Cond: `param("service_tier") == "fast"`,
|
|
Multiplier: 2,
|
|
Matched: true,
|
|
}}
|
|
other := buildTestLogOther(ctx, info, priceData, usage, &billingexpr.TieredResult{
|
|
MatchedTier: "base",
|
|
RequestRules: requestRules,
|
|
})
|
|
|
|
fields := other.Snapshot()
|
|
require.Equal(t, "tiered_expr", fields["billing_mode"])
|
|
require.Equal(t, "base", fields["matched_tier"])
|
|
require.Equal(t, requestRules, fields["request_rules"])
|
|
require.NotEmpty(t, fields["expr_b64"])
|
|
}
|
|
|
|
func TestResolveChannelTestUserIDUsesRequestUser(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
ctx.Set("id", 2)
|
|
|
|
userID, err := resolveChannelTestUserID(ctx)
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, userID)
|
|
}
|
|
|
|
func TestSelectChannelsForAutomaticTestPassiveRecoveryOnlyUsesAutoDisabled(t *testing.T) {
|
|
channels := []*model.Channel{
|
|
{Id: 1, Status: common.ChannelStatusEnabled},
|
|
{Id: 2, Status: common.ChannelStatusAutoDisabled},
|
|
{Id: 3, Status: common.ChannelStatusManuallyDisabled},
|
|
}
|
|
|
|
selected := selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModePassiveRecovery)
|
|
|
|
require.Len(t, selected, 1)
|
|
require.Equal(t, 2, selected[0].Id)
|
|
}
|
|
|
|
func TestSelectChannelsForAutomaticTestScheduledSkipsManualDisabled(t *testing.T) {
|
|
channels := []*model.Channel{
|
|
{Id: 1, Status: common.ChannelStatusEnabled},
|
|
{Id: 2, Status: common.ChannelStatusAutoDisabled},
|
|
{Id: 3, Status: common.ChannelStatusManuallyDisabled},
|
|
}
|
|
|
|
selected := selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModeScheduledAll)
|
|
|
|
require.Len(t, selected, 2)
|
|
require.Equal(t, 1, selected[0].Id)
|
|
require.Equal(t, 2, selected[1].Id)
|
|
}
|
|
|
|
func TestSelectChannelsForAutomaticTestAutoBanOnlyUsesEligibleChannels(t *testing.T) {
|
|
autoBanEnabled := 1
|
|
autoBanDisabled := 0
|
|
channels := []*model.Channel{
|
|
{Id: 1, Status: common.ChannelStatusEnabled, AutoBan: &autoBanEnabled},
|
|
{Id: 2, Status: common.ChannelStatusEnabled, AutoBan: &autoBanDisabled},
|
|
{Id: 3, Status: common.ChannelStatusAutoDisabled, AutoBan: &autoBanEnabled},
|
|
{Id: 4, Status: common.ChannelStatusManuallyDisabled, AutoBan: &autoBanEnabled},
|
|
{Id: 5, Status: common.ChannelStatusEnabled},
|
|
}
|
|
|
|
selected := selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModeAutoBanOnly)
|
|
|
|
require.Len(t, selected, 2)
|
|
require.Equal(t, 1, selected[0].Id)
|
|
require.Equal(t, 3, selected[1].Id)
|
|
}
|
|
|
|
func TestRunChannelTestWorkersHonorsConfiguredConcurrency(t *testing.T) {
|
|
originalInterval := common.RequestInterval
|
|
common.RequestInterval = 0
|
|
t.Cleanup(func() { common.RequestInterval = originalInterval })
|
|
|
|
channels := []*model.Channel{
|
|
{Id: 1, Status: common.ChannelStatusEnabled},
|
|
{Id: 2, Status: common.ChannelStatusEnabled},
|
|
{Id: 3, Status: common.ChannelStatusEnabled},
|
|
{Id: 4, Status: common.ChannelStatusEnabled},
|
|
}
|
|
started := make(chan struct{}, len(channels))
|
|
release := make(chan struct{})
|
|
var active atomic.Int32
|
|
var maxActive atomic.Int32
|
|
progress := make([]int, 0, len(channels)+1)
|
|
summaryResult := make(chan channelTestSummary, 1)
|
|
|
|
go func() {
|
|
summaryResult <- runChannelTestWorkers(
|
|
context.Background(),
|
|
channels,
|
|
2,
|
|
func(_ context.Context, _ *model.Channel) channelTestSummary {
|
|
current := active.Add(1)
|
|
defer active.Add(-1)
|
|
for {
|
|
observed := maxActive.Load()
|
|
if current <= observed || maxActive.CompareAndSwap(observed, current) {
|
|
break
|
|
}
|
|
}
|
|
started <- struct{}{}
|
|
<-release
|
|
return channelTestSummary{Tested: 1, Succeeded: 1}
|
|
},
|
|
func(processed, _ int) {
|
|
progress = append(progress, processed)
|
|
},
|
|
)
|
|
}()
|
|
|
|
<-started
|
|
<-started
|
|
select {
|
|
case <-started:
|
|
t.Fatal("started more channel tests than the configured concurrency")
|
|
default:
|
|
}
|
|
close(release)
|
|
|
|
summary := <-summaryResult
|
|
|
|
assert.Equal(t, int32(2), maxActive.Load())
|
|
assert.Equal(t, channelTestSummary{Tested: 4, Succeeded: 4}, summary)
|
|
assert.Equal(t, []int{0, 1, 2, 3, 4}, progress)
|
|
}
|
|
|
|
func TestRunChannelTestWorkersStopsAfterCancellation(t *testing.T) {
|
|
originalInterval := common.RequestInterval
|
|
common.RequestInterval = 0
|
|
t.Cleanup(func() { common.RequestInterval = originalInterval })
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
channels := []*model.Channel{
|
|
{Id: 1, Status: common.ChannelStatusEnabled},
|
|
{Id: 2, Status: common.ChannelStatusEnabled},
|
|
{Id: 3, Status: common.ChannelStatusEnabled},
|
|
{Id: 4, Status: common.ChannelStatusEnabled},
|
|
}
|
|
started := make(chan struct{}, len(channels))
|
|
progress := make([]int, 0, 1)
|
|
summaryResult := make(chan channelTestSummary, 1)
|
|
|
|
go func() {
|
|
summaryResult <- runChannelTestWorkers(
|
|
ctx,
|
|
channels,
|
|
2,
|
|
func(ctx context.Context, _ *model.Channel) channelTestSummary {
|
|
started <- struct{}{}
|
|
<-ctx.Done()
|
|
return channelTestSummary{Tested: 1, Succeeded: 1}
|
|
},
|
|
func(processed, _ int) {
|
|
progress = append(progress, processed)
|
|
},
|
|
)
|
|
}()
|
|
|
|
<-started
|
|
<-started
|
|
cancel()
|
|
|
|
summary := <-summaryResult
|
|
|
|
select {
|
|
case <-started:
|
|
t.Fatal("started another channel test after cancellation")
|
|
default:
|
|
}
|
|
assert.Equal(t, channelTestSummary{Tested: 2, Succeeded: 2}, summary)
|
|
assert.Equal(t, []int{0}, progress)
|
|
}
|
|
|
|
func TestTestAllChannelsRejectsExistingActiveTask(t *testing.T) {
|
|
db := setupModelListControllerTestDB(t)
|
|
require.NoError(t, db.AutoMigrate(&model.SystemTask{}, &model.SystemTaskLock{}))
|
|
|
|
existing, err := model.CreateSystemTask(model.SystemTaskTypeChannelTest, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
ctx.Request = httptest.NewRequest(http.MethodPost, "/api/channel/test", nil)
|
|
|
|
TestAllChannels(ctx)
|
|
|
|
require.Equal(t, http.StatusConflict, recorder.Code)
|
|
require.Contains(t, recorder.Body.String(), existing.TaskID)
|
|
require.Contains(t, recorder.Body.String(), "A channel test task is already running or queued")
|
|
}
|