package reports import ( "strings" "testing" "time" ) // TestEmptyPeriodProducesNoFabricatedNumbers ist der Kerntest der Berichte. // // Ein Zeitraum ohne Sicherungslauf ist der gefaehrlichste Fall: Genau dann, // wenn die Sicherung ausgefallen ist, darf der Bericht nicht den besten aller // Zustaende melden. Geprueft wird jede Kennzahl, die einen Lauf voraussetzt. // // „Laengste Laufzeit" stand hier zunaechst als gemessene Null — die Datenbank // liefert dank COALESCE eine, und im Bericht sah sie aus wie ein Lauf, der in // null Sekunden durchlief. Im Nachweis gegen die laufende Anlage aufgefallen. func TestEmptyPeriodProducesNoFabricatedNumbers(testInstance *testing.T) { emptyStatistics := runStatistics{} metricsRequiringARun := []Metric{ successRateMetric(emptyStatistics), averageDurationMetric(emptyStatistics), longestDurationMetric(emptyStatistics), } for _, metric := range metricsRequiringARun { if metric.IsKnown { testInstance.Errorf("%q gilt ohne einen einzigen Lauf als gemessen (Wert %g)", metric.Label, metric.Value) } if metric.UnknownReason == "" { testInstance.Errorf("%q nennt keinen Grund; eine leere Zelle ohne Begründung "+ "sieht aus wie ein Fehler des Werkzeugs", metric.Label) } if metric.Display() != "nicht gemessen" { testInstance.Errorf("%q wird als %q dargestellt", metric.Label, metric.Display()) } } } // TestSuccessRateExcludesRunningJobs prueft den Nenner der Erfolgsquote. // // Ein noch laufender Vorgang hat kein Ergebnis. Ihn in den Nenner zu nehmen // zoege die Quote nach unten, solange gerade gesichert wird — die Anlage saehe // waehrend ihrer Arbeit am schlechtesten aus. func TestSuccessRateExcludesRunningJobs(testInstance *testing.T) { statisticsWithRunning := runStatistics{ TotalRuns: 3, SucceededRuns: 2, RunningRuns: 1, } successRate := successRateMetric(statisticsWithRunning) if !successRate.IsKnown { testInstance.Fatal("die Erfolgsquote gilt trotz zweier abgeschlossener Läufe als ungemessen") } if successRate.Value != 100 { testInstance.Errorf("die Erfolgsquote ist %.1f Prozent, erwartet wurden 100 — "+ "der laufende Vorgang zählt in den Nenner", successRate.Value) } } // TestPartialFailureIsNotASuccess prueft die verbindliche Regel 1. // // Ein Teilfehler ist kein Erfolg. Ihn mitzuzaehlen waere die bequemste Art, eine // Quote zu schoenen — und der Bericht ist genau das Dokument, in dem diese // Unterscheidung gelesen wird. func TestPartialFailureIsNotASuccess(testInstance *testing.T) { statisticsWithPartial := runStatistics{ TotalRuns: 2, SucceededRuns: 1, PartialRuns: 1, } successRate := successRateMetric(statisticsWithPartial) if successRate.Value != 50 { testInstance.Errorf("die Erfolgsquote ist %.1f Prozent, erwartet wurden 50 — "+ "ein Teilfehler zählt als Erfolg", successRate.Value) } } // TestPointInTimeReportsIgnoreRequestedPeriod prueft den Zeitbezug. // // Ein Zustandsbericht ueber die Belegung „vom letzten Dienstag" kann es nicht // geben; die Belegung wird nicht historisiert. Wer einen Zeitraum angibt, // bekaeme sonst einen Zustandsbericht mit falscher Ueberschrift. func TestPointInTimeReportsIgnoreRequestedPeriod(testInstance *testing.T) { definition, found := FindDefinition(TypeRepositoryCapacity) if !found { testInstance.Fatal("die Reportart fehlt im Katalog") } requestedFrom := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) requestedTo := time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC) referenceTime := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC) periodFrom, periodTo, periodError := ResolvePeriod(definition, &requestedFrom, &requestedTo, referenceTime) if periodError != nil { testInstance.Fatalf("der Zeitraum ließ sich nicht bestimmen: %v", periodError) } if !periodFrom.Equal(referenceTime) || !periodTo.Equal(referenceTime) { testInstance.Errorf("der Zustandsbericht übernahm den angefragten Zeitraum %s bis %s", periodFrom, periodTo) } } // TestRangeReportRejectsInvertedPeriod prueft die Zeitraumpruefung. func TestRangeReportRejectsInvertedPeriod(testInstance *testing.T) { definition, _ := FindDefinition(TypeDailyBackup) laterTime := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC) earlierTime := laterTime.Add(-time.Hour) if _, _, periodError := ResolvePeriod(definition, &laterTime, &earlierTime, laterTime); periodError == nil { testInstance.Error("ein Zeitraum mit vertauschten Grenzen wurde angenommen") } } // TestDefaultPeriodApplies prueft den Standardzeitraum. func TestDefaultPeriodApplies(testInstance *testing.T) { definition, _ := FindDefinition(TypeWeeklyBackup) referenceTime := time.Date(2026, 8, 13, 12, 0, 0, 0, time.UTC) periodFrom, periodTo, periodError := ResolvePeriod(definition, nil, nil, referenceTime) if periodError != nil { testInstance.Fatalf("der Zeitraum ließ sich nicht bestimmen: %v", periodError) } if periodTo.Sub(periodFrom) != 7*24*time.Hour { testInstance.Errorf("der Standardzeitraum umfasst %v, erwartet wurden 7 Tage", periodTo.Sub(periodFrom)) } } // TestEveryReportTypeIsBuildable prueft die Vollstaendigkeit des Katalogs. // // Geprueft wird nicht die Zahl der Berichte, sondern dass jeder im Katalog // gefuehrte auch erzeugt werden kann. Ein Test auf „neun Stueck" bestuende // weiter, wenn ein Bericht durch einen anderen ersetzt wuerde — genau dieser // Fehler steckte im Diagrammtest aus Phase 13. func TestEveryReportTypeIsBuildable(testInstance *testing.T) { buildableTypes := map[ReportType]bool{ TypeDailyBackup: true, TypeWeeklyBackup: true, TypeMonthlyBackup: true, TypeFailedBackup: true, TypeRepositoryCapacity: true, TypeRecovery: true, TypeSecurity: true, TypeCompliance: true, TypeRecoveryObjectives: true, } for _, definition := range Definitions() { if !buildableTypes[definition.Type] { testInstance.Errorf("die Reportart %q steht im Katalog, wird aber nicht erzeugt", definition.Type) } if definition.Title == "" || definition.Description == "" { testInstance.Errorf("der Reportart %q fehlt Titel oder Beschreibung", definition.Type) } if len(definition.Formats) != 3 { testInstance.Errorf("die Reportart %q bietet %d Formate; jeder Bericht beherrscht drei", definition.Type, len(definition.Formats)) } // Ein Bericht ueber einen Zeitraum braucht einen Standardzeitraum, // sonst waere er ohne ausdrueckliche Angabe leer. if definition.PeriodKind == PeriodRange && definition.DefaultPeriod <= 0 { testInstance.Errorf("der Reportart %q fehlt der Standardzeitraum", definition.Type) } } } // TestRepositoryStatusLabelsMatchSchema prueft die Zustandsbezeichnungen. // // Das Repository-Widget der Phase 12 prueft auf Zustaende, die das Schema nie // kannte, und meldete „alle erreichbar" bei vier nicht erreichbaren. Ein // unbekannter Zustand wird deshalb durchgereicht statt auf ein freundliches // Wort abgebildet. func TestRepositoryStatusLabelsMatchSchema(testInstance *testing.T) { for _, schemaStatus := range []string{"active", "read_only", "unavailable", "maintenance"} { if repositoryStatusLabel(schemaStatus) == schemaStatus { testInstance.Errorf("der Zustand %q wird nicht übersetzt", schemaStatus) } } // Ein kuenftiger Zustand darf nicht hinter „unbekannt" verschwinden. if repositoryStatusLabel("archived") != "archived" { testInstance.Error("ein unbekannter Zustand wird auf ein freundliches Wort abgebildet") } } // TestShareMetricDistinguishesZeroFromNothing prueft den leeren Nenner. // // Null von null ist nicht null Prozent, sondern gar nichts. „0 % verschlüsselt" // meldet einen Missstand; tatsaechlich gab es nichts zu verschluesseln. func TestShareMetricDistinguishesZeroFromNothing(testInstance *testing.T) { emptyShare := shareMetric("Verschlüsselt", 0, 0, "Es gibt keinen Wiederherstellungspunkt.") if emptyShare.IsKnown { testInstance.Error("ein Anteil ohne Grundgesamtheit gilt als gemessen") } measuredZero := shareMetric("Verschlüsselt", 0, 10, "Es gibt keinen Wiederherstellungspunkt.") if !measuredZero.IsKnown { testInstance.Error("null von zehn gilt als ungemessen") } if measuredZero.Value != 0 { testInstance.Errorf("null von zehn ergab %.1f Prozent", measuredZero.Value) } } // TestReportTextsCarryNoMarkup prueft die Textauszeichnung der Berichte. // // Ein Sternchenpaar, das im Browser fett wuerde, steht in der PDF-Datei // woertlich da — im Nachweis erschien „**keine Zertifizierung**" mitten im // Bericht fuer Pruefungen. Das Modell ist formatunabhaengig; sein Text darf // deshalb kein Format tragen. func TestReportTextsCarryNoMarkup(testInstance *testing.T) { for _, definition := range Definitions() { if strings.Contains(definition.Description, "**") || strings.Contains(definition.Title, "**") { testInstance.Errorf("die Reportart %q trägt Auszeichnung im Text", definition.Type) } } // Auch die Hinweise, die erst beim Bauen entstehen. sampleReport := &Report{} sampleReport.AddNote("Ein Hinweis ohne Auszeichnung.") for _, note := range sampleReport.Notes { if strings.Contains(note, "**") { testInstance.Errorf("der Hinweis %q trägt Auszeichnung", note) } } }