package services import ( "context" "strings" "testing" "time" "fwh-toolbox-server/domain" "fwh-toolbox-server/repositories" ) func TestFaultSubMatchesCallable(t *testing.T) { candidate := domain.FaultRecordCallableInfo{FailureTime: "2026-09-22 12:30:00", TriggerSourceCode: "E42"} sub := domain.FaultRecordSubInfo{FailureTime: "2026-09-22 12:30:00", EventTriggerSource: "E42"} if !faultSubMatchesCallable(sub, candidate) { t.Fatal("matching time and code should match") } sub.EventTriggerSource = "E43" if faultSubMatchesCallable(sub, candidate) { t.Fatal("different fault code must not match") } } func TestExistingFaultRecordRequiresCompleteSubList(t *testing.T) { candidates := []domain.FaultRecordCallableInfo{{FailureTime: "2026-09-22 12:30:00", TriggerSourceCode: "E42"}} subs := []domain.FaultRecordSubInfo{{Id: 10, FailureTime: candidates[0].FailureTime, EventTriggerSource: candidates[0].TriggerSourceCode}} matched := []bool{false} record := domain.FaultRecordInfo{Id: 99, FaultRecordsCount: 2} if got := matchExistingFaultRecord(record, subs, candidates, matched); len(got) != 0 || matched[0] { t.Fatalf("incomplete record was reused: %+v", got) } record.FaultRecordsCount = 1 if got := matchExistingFaultRecord(record, subs, candidates, matched); len(got) != 1 || !matched[0] { t.Fatalf("complete record not reused: %+v", got) } } func TestResolveFaultRecordSourcesReusesVerifiedSubrecord(t *testing.T) { originalCatalog := repositories.QueryFaultRecordCallableList originalList := repositories.QueryFaultRecordList originalSubs := repositories.QueryFaultRecordSubList t.Cleanup(func() { repositories.QueryFaultRecordCallableList = originalCatalog repositories.QueryFaultRecordList = originalList repositories.QueryFaultRecordSubList = originalSubs }) repositories.QueryFaultRecordCallableList = func(_ int, _ string) ([]domain.FaultRecordCallableInfo, error) { return []domain.FaultRecordCallableInfo{{Idx: 7, FailureTime: "2026-09-22 12:30:00", TriggerSourceCode: "E42"}}, nil } repositories.QueryFaultRecordList = func(_, _, _ string) ([]domain.FaultRecordInfo, error) { return []domain.FaultRecordInfo{{Id: 99, GatewayId: "GATE-1", DeviceType: 1, FaultRecordsCount: 1}}, nil } repositories.QueryFaultRecordSubList = func(_ int64) ([]domain.FaultRecordSubInfo, error) { return []domain.FaultRecordSubInfo{{Id: 10, FaultId: 99, GatewayId: "GATE-1", FailureTime: "2026-09-22 12:30:00", EventTriggerSource: "E42"}}, nil } start := time.Date(2026, 9, 22, 0, 0, 0, 0, time.UTC) sources, err := resolveFaultRecordSources(context.Background(), &domain.DataDownloadItem{DeviceSN: "GATE-1"}, "document-1", 1, start, start.Add(24*time.Hour-time.Second)) if err != nil { t.Fatal(err) } if len(sources) != 1 || sources[0].sub.Id != 10 { t.Fatalf("sources = %+v", sources) } } func TestFindFaultCallByRequestChecksAllIdentityFields(t *testing.T) { original := repositories.QueryFaultRecordCallRecordPageContext t.Cleanup(func() { repositories.QueryFaultRecordCallRecordPageContext = original }) sn := "GATE-1" other := "GATE-2" status := 0 calledAt := time.Date(2026, 9, 23, 12, 0, 0, 0, time.UTC) repositories.QueryFaultRecordCallRecordPageContext = func(_ context.Context, page, size int) ([]domain.FaultRecordInfo, error) { if page > 1 { return nil, nil } return []domain.FaultRecordInfo{ {Id: 101, DeviceType: 1, GatewayId: other, FaultRecordsCount: 2, TriggerType: 2, CellTime: "2026-09-23 12:00:00", Status: &status}, {Id: 102, DeviceType: 1, GatewayId: sn, FaultRecordsCount: 1, TriggerType: 2, CellTime: "2026-09-23 12:00:00", Status: &status}, {Id: 103, DeviceType: 1, GatewayId: sn, FaultRecordsCount: 2, TriggerType: 1, CellTime: "2026-09-23 12:00:00", Status: &status}, {Id: 104, DeviceType: 1, GatewayId: sn, FaultRecordsCount: 2, TriggerType: 2, CellTime: "2026-09-23 12:02:00", Status: &status}, {Id: 105, DeviceType: 1, GatewayId: sn, FaultRecordsCount: 2, TriggerType: 2, CellTime: "2026-09-23 12:00:30", Status: &status}, }, nil } record, err := findFaultCallByRequest(context.Background(), sn, 1, 2, calledAt) if err != nil { t.Fatal(err) } if record == nil || record.Id != 105 { t.Fatalf("got %+v, want ID 105", record) } } func TestFindFaultCallByIDUsesPageSearch(t *testing.T) { original := repositories.QueryFaultRecordCallRecordPageContext t.Cleanup(func() { repositories.QueryFaultRecordCallRecordPageContext = original }) queries := 0 repositories.QueryFaultRecordCallRecordPageContext = func(_ context.Context, page, size int) ([]domain.FaultRecordInfo, error) { queries++ if page > 128 { return nil, nil } rows := make([]domain.FaultRecordInfo, size) for i := range rows { rows[i].Id = int64(6401 - (page-1)*size - i) } return rows, nil } record, err := findFaultCallByID(context.Background(), 1600) if err != nil { t.Fatal(err) } if record == nil || record.Id != 1600 { t.Fatalf("got %+v, want ID 1600", record) } if queries > 16 { t.Fatalf("used %d queries for a record on page 97", queries) } } func TestSummonFaultRecordsHonorsElapsedTimeout(t *testing.T) { started := time.Now().UTC().Add(-faultCallTimeout - time.Minute) item := &domain.DataDownloadItem{CallRequestedAt: &started, DeviceSN: "GATE-1"} _, err := summonFaultRecords(context.Background(), item, "document-1", 1, []int{1}) if err == nil || !strings.Contains(err.Error(), "timed out after 2h") { t.Fatalf("got %v, want timeout", err) } } func TestActiveFaultCallKeepsOwnershipAfterCancellation(t *testing.T) { started := time.Now().UTC().Add(-time.Minute) item := &domain.DataDownloadItem{Kind: domain.DataDownloadFaultRecord, CallRequestedAt: &started} if err := ReleaseFaultCallOwnership(context.Background(), item); err != nil { t.Fatal(err) } }