Files
member-console/cmd/integration_config_parity_test.go
T
cgalo5758 0b28a9dc29 Remediate security audit findings
- Replace gorilla/csrf with net/http CrossOriginProtection
- Require valkey-password and add TLS options for session store
- End session at /logout and revoke refresh tokens
- Re-derive identity and roles from provider every five minutes
- Process each Stripe webhook event in its own Temporal workflow
- Give each outbox entry its own workflow with Temporal retries
- Guard against stale Stripe events with provider timestamps
- Derive transport security from base-url scheme
2026-09-09 13:25:43 -05:00

154 lines
5.9 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package cmd
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
"git.coopcloud.tech/wiki-cafe/member-console/internal/integrations"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// TestRegisterIntegrationConfigFlagsTypes covers the typed ConfigSpec seam
// (integration-config-parity D2): bool and duration keys declared by an
// integration register as flags of the matching type with their declared
// defaults, so no integration knob needs hand-declaring in core.
func TestRegisterIntegrationConfigFlagsTypes(t *testing.T) {
cmd := &cobra.Command{Use: "probe"}
registerIntegrationConfigFlags(cmd)
wantTypes := map[string]string{
"fedwiki-sync-enabled": "bool",
"fedwiki-sync-interval": "duration",
"fedwiki-sync-trigger-immediately": "bool",
"fedwiki-swap-cooldown": "duration",
"discourse-sync-interval": "duration",
"discourse-sync-trigger-immediately": "bool",
"fedwiki-farm-api-url": "string",
"fedwiki-allowed-domains": "stringSlice",
}
for name, wantType := range wantTypes {
f := cmd.Flags().Lookup(name)
if f == nil {
t.Errorf("flag %q not registered", name)
continue
}
if got := f.Value.Type(); got != wantType {
t.Errorf("flag %q type = %q, want %q", name, got, wantType)
}
}
// Declared defaults flow through viper (SetDefault runs in registration).
if got := viper.GetDuration("fedwiki-sync-interval"); got != time.Hour {
t.Errorf("fedwiki-sync-interval default = %v, want 1h", got)
}
if got := viper.GetBool("fedwiki-sync-trigger-immediately"); got != true {
t.Errorf("fedwiki-sync-trigger-immediately default = %v, want true", got)
}
if got := viper.GetDuration("fedwiki-swap-cooldown"); got != 30*24*time.Hour {
t.Errorf("fedwiki-swap-cooldown default = %v, want 720h", got)
}
if got := viper.GetDuration("discourse-sync-interval"); got != 15*time.Minute {
t.Errorf("discourse-sync-interval default = %v, want 15m", got)
}
if got := viper.GetBool("discourse-sync-trigger-immediately"); got != false {
t.Errorf("discourse-sync-trigger-immediately default = %v, want false", got)
}
// The connect target is a core key, not any integration's: no integration
// declares it, and the old integration-namespaced name is gone.
if f := cmd.Flags().Lookup("fedwiki-custom-domain-target"); f != nil {
t.Error("fedwiki-custom-domain-target still registers via an integration ConfigSpec; the key was replaced by core domains-connect-target")
}
}
// TestCheckSpecsAgainstInstalledIntegrations guards every installed
// integration's own ConfigSpec against a declaration error (design D1, D6,
// typed-config-keys): a Type that disagrees with its default, a TypeEnum
// with no Enum members, an Enum with a non-string default, or a Positive on
// a key that cannot carry it. cmd/start.go runs the same check at boot,
// before ValidateStart; this test catches a bad declaration in CI without
// booting the app.
func TestCheckSpecsAgainstInstalledIntegrations(t *testing.T) {
var specs []config.ConfigKey
for _, integ := range integrations.All() {
if cp, ok := integ.(config.ConfigProvider); ok {
specs = append(specs, cp.ConfigSpec()...)
}
}
if err := config.CheckSpecs(specs); err != nil {
t.Errorf("an installed integration's ConfigSpec fails CheckSpecs: %v", err)
}
}
// TestInitScaffoldGeneratedSections covers the console-init delta: the
// scaffold is the embedded core template plus one generated section per
// declaring integration, stays valid YAML, and hand-lists no integration key.
func TestInitScaffoldGeneratedSections(t *testing.T) {
template, err := embeds.Config.ReadFile("mc-config.yaml")
if err != nil {
t.Fatalf("read embedded template: %v", err)
}
sections := integrationConfigSections()
scaffold := string(template) + sections
// Every registered declaring integration appears with every declared key.
declaring := 0
for _, integ := range integrations.All() {
cp, ok := integ.(config.ConfigProvider)
if !ok {
continue
}
declaring++
displayName := integ.Provider().ProviderManifest().DisplayName
if !strings.Contains(sections, fmt.Sprintf("Optional: %s integration", displayName)) {
t.Errorf("generated sections missing header for %s", displayName)
}
for _, key := range cp.ConfigSpec() {
if !strings.Contains(sections, "# "+key.Name+":") {
t.Errorf("generated sections missing key %q", key.Name)
}
if key.Secret && !strings.Contains(sections, "# "+key.Name+"-file:") {
t.Errorf("generated sections missing secret file variant for %q", key.Name)
}
}
}
if declaring == 0 {
t.Fatal("no declaring integrations registered; test is vacuous")
}
// The embedded core template hand-lists no integration key (generation
// owns them), while required core keys stay.
for _, banned := range []string{"\nstripe-api-key:", "\nfedwiki-farm-api-url:", "\ndiscourse-base-url:"} {
if strings.Contains(string(template), banned) {
t.Errorf("embedded template still hand-lists integration key %q", strings.TrimSpace(banned))
}
}
for _, required := range []string{"base-url:", "db-dsn:", "valkey-addr:", "valkey-password:", "oidc-idp-issuer-url:", "oidc-sp-client-id:"} {
if !strings.Contains(scaffold, required) {
t.Errorf("scaffold missing required core key %q", required)
}
}
if strings.Contains(scaffold, "realms/master") {
t.Error("scaffold references an administration realm (realms/master)")
}
// The whole scaffold parses as YAML (generated lines are comments).
v := viper.New()
v.SetConfigType("yaml")
if err := v.ReadConfig(bytes.NewReader([]byte(scaffold))); err != nil {
t.Fatalf("scaffold does not parse as YAML: %v", err)
}
if got := v.GetString("base-url"); got == "" {
t.Error("parsed scaffold lost base-url")
}
}