427 lines
23 KiB
Go
427 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/epistemics"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
"git.cin.su/m/Assistant/contracts/work"
|
|
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
|
)
|
|
|
|
func TestHumanResearchExcerptRemovesSearchResultHeading(t *testing.T) {
|
|
got := humanResearchExcerpt("погода в Санкт-Петербурге", "Погода в Санкт-Петербурге — Прогноз погоды — Сейчас пасмурно, температура +14°")
|
|
if got != "Сейчас пасмурно, температура +14°" || strings.Contains(got, "Прогноз погоды") {
|
|
t.Fatalf("human research excerpt = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestNearestFlightEvidenceDoesNotClaimMissingDepartureTime(t *testing.T) {
|
|
if !nearestFlightQuery("ближайший рейс Москва Казань сегодня") {
|
|
t.Fatal("nearest flight query was not recognized")
|
|
}
|
|
if containsFlightTimeEvidence("Казань составляет 1 ч 25 мин. График вылетов доступен онлайн") {
|
|
t.Fatal("duration was mistaken for a departure time")
|
|
}
|
|
if !containsFlightTimeEvidence("Ближайший вылет в 18:40") {
|
|
t.Fatal("departure time was not recognized")
|
|
}
|
|
}
|
|
|
|
func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
|
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://naidy.example", Title: "Мебельная фурнитура"}, Excerpt: epistemics.SourceExcerpt{Text: "Комплектующие для мебели оптом и в розницу."}},
|
|
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://rasp.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание самолётов Москва — Казань, время вылета и прилёта."}},
|
|
}}
|
|
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
|
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") {
|
|
t.Fatalf("unrelated source leaked into flight reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
|
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
|
|
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
|
|
}}
|
|
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
|
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
|
|
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
|
|
query := "ближайшие авиарейсы Москва Казань сегодня"
|
|
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
|
|
if !researchFindingMatchesIntent(query, matched) {
|
|
t.Fatal("route evidence with both endpoints was rejected")
|
|
}
|
|
unrelated := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань", Locator: "https://example.test/moscow-kazan"}, Excerpt: epistemics.SourceExcerpt{Text: "Рейсы Москва — Казань — Ближайшие к Казани города; цена указана ориентировочно."}}
|
|
if researchFindingMatchesIntent(query, unrelated) {
|
|
t.Fatal("one-ended travel evidence was accepted")
|
|
}
|
|
}
|
|
|
|
func TestTicketReplyRejectsGenericCategoryPageWithoutDestination(t *testing.T) {
|
|
query := "найди билеты до Ялты сегодня"
|
|
generic := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Купить билеты", Locator: "https://tickets.example"}, Excerpt: epistemics.SourceExcerpt{Text: "Поиск авиабилетов среди множества направлений."}}
|
|
if researchFindingMatchesIntent(query, generic) {
|
|
t.Fatal("generic ticket category was presented as destination result")
|
|
}
|
|
relevant := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Билеты"}, Excerpt: epistemics.SourceExcerpt{Text: "Билеты до Ялты: варианты на сегодня."}}
|
|
if !researchFindingMatchesIntent(query, relevant) {
|
|
t.Fatal("destination-specific ticket result was rejected")
|
|
}
|
|
}
|
|
|
|
func TestDistanceReplyRejectsCalculatorPageWithoutDistance(t *testing.T) {
|
|
query := "расстояние между Москве и Сочи в километрах по прямой и по дороге"
|
|
calculator := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Расчет расстояний"}, Excerpt: epistemics.SourceExcerpt{Text: "Калькулятор рассчитывает точное расстояние между городами."}}
|
|
if researchFindingMatchesIntent(query, calculator) {
|
|
t.Fatal("distance calculator landing page was presented without a measured distance")
|
|
}
|
|
measured := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Маршрут"}, Excerpt: epistemics.SourceExcerpt{Text: "Расстояние по дороге составляет 1 620 км."}}
|
|
if !researchFindingMatchesIntent(query, measured) {
|
|
t.Fatal("measured distance evidence was rejected")
|
|
}
|
|
}
|
|
|
|
func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
|
State: epistemics.FindingSupported,
|
|
Source: epistemics.Source{Locator: "https://market.example", Title: "Интернет-магазин"},
|
|
Excerpt: epistemics.SourceExcerpt{Text: "Широкий ассортимент товаров и услуг."},
|
|
}}}
|
|
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
|
|
if backed || !strings.Contains(reply, "marka-wear") || strings.Contains(reply, "wildberries") || !strings.Contains(reply, "уточните написание") {
|
|
t.Fatalf("generic page was presented as a brand result: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
|
State: epistemics.FindingSupported,
|
|
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
|
|
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
|
|
}}}
|
|
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
|
|
if backed || !strings.Contains(reply, "Проверяемых данных") {
|
|
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
|
State: epistemics.FindingSupported,
|
|
Source: epistemics.Source{Locator: "https://market.example/marka-wear", Title: "Marka-wear"},
|
|
Excerpt: epistemics.SourceExcerpt{Text: "Бренд marka-wear: товары и карточки."},
|
|
}}}
|
|
reply, backed := renderResearchReply("на WB бренд marka-wear", result, nil)
|
|
if !backed || !strings.Contains(strings.ToLower(reply), "marka-wear") {
|
|
t.Fatalf("concrete identifier finding was rejected: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestResearchIdentifiersExcludeSearchDate(t *testing.T) {
|
|
identifiers := requestedResearchIdentifiers("ближайший поезд в Петербург сегодня 26.08.2026")
|
|
if len(identifiers) != 0 {
|
|
t.Fatalf("search date was treated as a concrete identifier: %#v", identifiers)
|
|
}
|
|
}
|
|
|
|
func TestResearchQueryKeepsDistanceRelationBeforeSearchNormalization(t *testing.T) {
|
|
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
|
query := interactiondomain.TravelResearchQuery("сколько километров до Сочи?", history, time.Time{})
|
|
query = interactiondomain.SearchQueryWithLexicon(query, interactiondomain.TaskLexicon{})
|
|
if query != "расстояние между Москве и Сочи в километрах по прямой и по дороге" {
|
|
t.Fatalf("distance query lost remembered origin: %q", query)
|
|
}
|
|
}
|
|
|
|
func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
|
|
if reviewMayReplacePlanningReply(false, false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
|
|
t.Fatal("review replaced an honest unbacked research outcome")
|
|
}
|
|
if !reviewMayReplacePlanningReply(false, false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
|
|
t.Fatal("review should be able to improve a source-backed research outcome")
|
|
}
|
|
if reviewMayReplacePlanningReply(false, false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
|
|
t.Fatal("review dropped the research evidence")
|
|
}
|
|
if !reviewMayReplacePlanningReply(false, false, false, false, "Нормальный ответ", "Черновик") {
|
|
t.Fatal("ordinary conversation review was unexpectedly blocked")
|
|
}
|
|
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
|
|
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
|
|
}
|
|
// The caller must not mark a source-backed research result as immutable
|
|
// semantic state: it may be rewritten, but only with its evidence retained.
|
|
if !reviewMayReplacePlanningReply(false, false, true, true, "Из Москвы в Сочи можно добраться самолётом или поездом. Источник: https://example.test", "Сырые результаты. Источник: https://example.test") {
|
|
t.Fatal("source-backed research draft must be eligible for human-facing review")
|
|
}
|
|
}
|
|
|
|
func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
|
|
semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}}
|
|
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
|
|
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
|
|
}
|
|
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
|
|
t.Fatal("reply-only cognition path was incorrectly rejected")
|
|
}
|
|
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
|
|
t.Fatal("direct capability was incorrectly replaced by a textual reply")
|
|
}
|
|
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
|
|
t.Fatal("media route was incorrectly replaced by a textual reply")
|
|
}
|
|
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) {
|
|
t.Fatal("durable work was incorrectly replaced by a textual reply")
|
|
}
|
|
}
|
|
|
|
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
|
request := httptest.NewRequest("GET", "/", nil)
|
|
request.Header.Set("Authorization", "Basic value")
|
|
if token := bearerToken(request); token != "" {
|
|
t.Fatalf("unexpected token %q", token)
|
|
}
|
|
request.Header.Set("Authorization", "Bearer creator-session")
|
|
if token := bearerToken(request); token != "creator-session" {
|
|
t.Fatalf("token = %q", token)
|
|
}
|
|
}
|
|
|
|
func TestLatestAssistantNameUsesNewestDurablePreference(t *testing.T) {
|
|
history := []interaction.HumanUtterance{
|
|
{Content: "я назову тебя Дед", ReceivedAtUnixMillis: 20},
|
|
{Content: "теперь тебя зовут Фея", ReceivedAtUnixMillis: 30},
|
|
}
|
|
if got := latestAssistantName(history); got != "Фея" {
|
|
t.Fatalf("latest assistant name = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) {
|
|
history := []interaction.HumanUtterance{
|
|
{Content: "мне нравится синий цвет", ReceivedAtUnixMillis: 20},
|
|
{Content: "мне нравится красный цвет", ReceivedAtUnixMillis: 30},
|
|
}
|
|
got := humanMemoryInventory(history, "Фея")
|
|
if !strings.Contains(got, "красный") || strings.Contains(got, "синий") {
|
|
t.Fatalf("memory inventory did not select newest preference: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestContainsScheduleTimeEvidence(t *testing.T) {
|
|
if containsScheduleTimeEvidence("расписание поездов и цена билета") {
|
|
t.Fatal("a price or schedule label is not a departure time")
|
|
}
|
|
if !containsScheduleTimeEvidence("Отправление в 07:45") {
|
|
t.Fatal("expected an explicit departure time to be recognized")
|
|
}
|
|
}
|
|
|
|
func TestInternetStatusQueryRecognizesConnectionChecks(t *testing.T) {
|
|
for _, query := range []string{"проверь, работает ли интернет", "есть доступ к сети?"} {
|
|
if !isInternetStatusQuery(query) {
|
|
t.Fatalf("connection check was not recognized: %q", query)
|
|
}
|
|
}
|
|
if isInternetStatusQuery("найди интернет-магазин") {
|
|
t.Fatal("ordinary search must not be treated as a connectivity check")
|
|
}
|
|
}
|
|
|
|
func TestInternetStatusReplyDoesNotLeakIncidentalSearchResults(t *testing.T) {
|
|
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
|
State: epistemics.FindingSupported,
|
|
Source: epistemics.Source{Locator: "https://example.test", Title: "Incidental page"},
|
|
Excerpt: epistemics.SourceExcerpt{Text: "A response was received."},
|
|
}}}
|
|
reply, backed := renderResearchReply("проверь, работает ли интернет", result, nil)
|
|
if !backed || reply != "Интернет работает: Домовой получил ответ от внешнего источника." {
|
|
t.Fatalf("unexpected internet status reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
|
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
|
|
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
|
|
t.Fatalf("unexpected safe reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCurrentModelReplyDoesNotInventProvider(t *testing.T) {
|
|
reply := humanCurrentCognitiveModelReply(capability.Health{State: capability.HealthDegraded, CapabilityID: "provider-secret-model"}, nil)
|
|
if !strings.Contains(reply, "встроенным режимом") || !strings.Contains(reply, "ограничениями") || strings.Contains(reply, "provider-secret-model") {
|
|
t.Fatalf("unexpected current-model reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
|
|
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
|
|
if reply != "Канал мышления недоступен. Домовой остаётся на связи." {
|
|
t.Fatalf("unexpected configuration reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
|
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
|
|
if strings.Contains(reply, "когнитивный канал") || strings.Contains(reply, "Повторю") || !strings.Contains(reply, "не выдумываю") {
|
|
t.Fatalf("unexpected degraded reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
|
|
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
|
|
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
|
|
if err != nil {
|
|
t.Fatalf("interpretation failed for %q: %v", message, err)
|
|
}
|
|
if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory {
|
|
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
|
|
}
|
|
}
|
|
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory {
|
|
t.Fatal("configuration mutation must not use read-only model inventory path")
|
|
}
|
|
}
|
|
|
|
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
|
|
cost := int64(25)
|
|
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
|
|
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
|
|
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
|
|
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
|
|
}}, nil)
|
|
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "активна") {
|
|
t.Fatalf("unsafe or incomplete inventory: %q", text)
|
|
}
|
|
}
|
|
|
|
func TestHumanCognitiveModelInventoryDeduplicatesAndPrioritizesAvailable(t *testing.T) {
|
|
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{
|
|
{Binding: capability.CognitiveBinding{Activation: capability.ActivationInactive}, Provider: capability.ProviderInstance{Health: capability.HealthUnavailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthUnavailable}},
|
|
{Binding: capability.CognitiveBinding{Activation: capability.ActivationActive}, Provider: capability.ProviderInstance{Health: capability.HealthAvailable}, Model: capability.ModelDescriptor{ProviderModelID: "same-model", Health: capability.HealthAvailable}},
|
|
}, nil)
|
|
if strings.Count(text, "same-model") != 1 || !strings.Contains(text, "same-model — активна") {
|
|
t.Fatalf("expected one active model entry, got %q", text)
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
|
|
}
|
|
var issued struct {
|
|
Token string
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&issued); err != nil || issued.Token == "" {
|
|
t.Fatalf("session response = %+v, err=%v", issued, err)
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerFailsClosed(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), time.Now)
|
|
tests := []string{
|
|
`{"username":"wrong","password":"password"}`,
|
|
`{"username":"m","password":"wrong"}`,
|
|
`{"username":"","password":""}`,
|
|
`{"username":"m","password":"password","actor":"attacker"}`,
|
|
`{`,
|
|
`{"username":"m","password":"password"}{}`,
|
|
}
|
|
for _, body := range tests {
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(body))
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusUnauthorized || strings.TrimSpace(response.Body.String()) != "creator authentication denied" {
|
|
t.Fatalf("body %q: status=%d response=%q", body, response.Code, response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerPreservesBootstrapRecovery(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "", "", []byte("01234567890123456789012345678901"), time.Now)
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", nil)
|
|
request.Header.Set("Authorization", "Bearer recovery")
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("bootstrap recovery status = %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFromEnvironment(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET", "direct-value")
|
|
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
|
if err != nil || value != "direct-value" {
|
|
t.Fatalf("value=%q err=%v", value, err)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFromFile(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, []byte("file-value\r\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
|
if err != nil || value != "file-value" {
|
|
t.Fatalf("value=%q err=%v", value, err)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFailsClosed(t *testing.T) {
|
|
t.Run("both sources", func(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET", "direct")
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "secret"))
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected ambiguous configuration to fail")
|
|
}
|
|
})
|
|
t.Run("empty file", func(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected empty secret to fail")
|
|
}
|
|
})
|
|
t.Run("multiline file", func(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, []byte("first\nsecond\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected multiline secret to fail")
|
|
}
|
|
})
|
|
t.Run("missing file", func(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "missing"))
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected missing secret file to fail")
|
|
}
|
|
})
|
|
}
|