package app import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestCodeExchangeFailureDetailsRetainsOnlySafeDiagnostics(t *testing.T) { t.Parallel() code := "TAPM-12345-ABCDE" details := codeExchangeFailureDetails(code, errCodeNotFound) for _, expected := range []string{ "reason=code_not_found", "code_hint=ABCDE", "normalized_length=16", "format=expected", } { if !strings.Contains(details, expected) { t.Errorf("details %q do not contain %q", details, expected) } } if strings.Contains(details, code) || strings.Contains(details, "12345") { t.Fatalf("details contain more of the submitted code than its hint: %q", details) } } func TestCodeExchangeFailureDetailsDoesNotHintUnrelatedInput(t *testing.T) { t.Parallel() details := codeExchangeFailureDetails("not-a-deployment-secret", errCodeNotFound) if !strings.Contains(details, "code_hint=unavailable") || !strings.Contains(details, "format=unexpected") { t.Fatalf("unexpected details for malformed input: %q", details) } if strings.Contains(details, "ecret") { t.Fatalf("details retained part of unrelated input: %q", details) } } func TestCodeExchangeFailureReasons(t *testing.T) { t.Parallel() tests := []struct { err error reason string }{ {errCodeExpired, "code_expired"}, {errCodeRevoked, "code_revoked"}, {errActionNotAuthorized, "action_not_authorized"}, {errPackageNotAuthorized, "package_not_authorized"}, {errHostLimitReached, "host_limit_reached"}, } for _, test := range tests { if details := codeExchangeFailureDetails("TAPM-12345-ABCDE", test.err); !strings.Contains(details, "reason="+test.reason) { t.Errorf("details %q do not contain reason %q", details, test.reason) } } } func TestFailedExpiredCodeAttemptIsLinkedToDeployment(t *testing.T) { server, _ := newAuthorizationTestServer(t) code := "TAPM-12345-ABCDE" codeHash := hashValue(code) if _, err := server.db.Exec( `INSERT INTO authorizations (id, code_hash, code_hint, created_by, customer_label, host_limit, expires_at) VALUES (42, ?, 'ABCDE', 'taiadmin', 'Acme migration', 3, ?)`, codeHash[:], time.Now().UTC().Add(-time.Hour), ); err != nil { t.Fatal(err) } body, err := json.Marshal(exchangeRequest{ Code: code, HostFingerprint: "0123456789abcdef", Hostname: "pve01", }) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/v1/exchange", bytes.NewReader(body)) response := httptest.NewRecorder() server.handleExchange(response, request) if response.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden) } var authorizationID uint64 var hostname, details string if err := server.db.QueryRow( `SELECT authorization_id, hostname, details FROM audit_events WHERE event_type = 'code_exchange_failed'`, ).Scan(&authorizationID, &hostname, &details); err != nil { t.Fatal(err) } if authorizationID != 42 || hostname != "pve01" { t.Fatalf("audit event authorization/hostname = %d/%q", authorizationID, hostname) } if !strings.Contains(details, "reason=code_expired") || !strings.Contains(details, "code_hint=ABCDE") || strings.Contains(details, code) { t.Fatalf("unsafe or incomplete audit details: %q", details) } }