package jobs import ( "net/url" "os" "testing" "time" "github.com/go-fed/activity/streams" "github.com/owncast/owncast/activitypub/apmodels" "github.com/owncast/owncast/activitypub/persistence" "github.com/owncast/owncast/activitypub/persistence/followersrepository" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/utils" ) func TestMain(m *testing.M) { setup() code := m.Run() os.Exit(code) } var datastore *data.Datastore func setup() { resetTestDatabase() } // resetTestDatabase initializes a fresh in-memory database for testing. func resetTestDatabase() { data.SetupPersistence(":memory:") datastore = data.GetDatastore() persistence.Setup(datastore) } // setupTestWithRepo resets the database and returns a new repository instance. func setupTestWithRepo(t *testing.T) followersrepository.FollowersRepository { t.Helper() resetTestDatabase() return followersrepository.New(datastore) } func createTestFollower(repo followersrepository.FollowersRepository, iri, inbox, name, username string) { actorIRI, _ := url.Parse(iri) inboxURL, _ := url.Parse(inbox) requestIRI, _ := url.Parse("https://fake.server/follow/request") fakeRequest := streams.NewActivityStreamsFollow() repo.Add(apmodels.ActivityPubActor{ ActorIri: actorIRI, Inbox: inboxURL, Name: name, Username: username, FullUsername: username + "@fake.server", FollowRequestIri: requestIRI, RequestObject: fakeRequest, }, true) } func TestGetFollowersToValidate(t *testing.T) { repo := setupTestWithRepo(t) // Create some test followers for i := 0; i < 10; i++ { user, _ := utils.GenerateRandomString(10) createTestFollower(repo, "https://fake.server/user/"+user, "https://fake.server/user/"+user+"/inbox", user, user) } // Get followers to validate followers, err := repo.GetFollowersToValidate(5) if err != nil { t.Fatalf("Error getting followers to validate: %s", err) } if len(followers) != 5 { t.Errorf("Expected 5 followers to validate, got %d", len(followers)) } } func TestUpdateFollowerValidationSuccess(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testuser" createTestFollower(repo, testIRI, "https://fake.server/user/testuser/inbox", "Test User", "testuser") // Mark validation as successful err := repo.UpdateFollowerValidationSuccess(testIRI) if err != nil { t.Fatalf("Error updating follower validation success: %s", err) } // Verify the follower was updated followers, err := repo.GetFollowersToValidate(10) if err != nil { t.Fatalf("Error getting followers: %s", err) } // After validation success, FirstValidationFailureAt should be NULL/invalid for _, f := range followers { if f.ActorIRI == testIRI { if f.FirstValidationFailureAt.Valid { t.Error("Expected FirstValidationFailureAt to be NULL after success") } } } } func TestUpdateFollowerValidationFailure(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testuser" createTestFollower(repo, testIRI, "https://fake.server/user/testuser/inbox", "Test User", "testuser") // Mark validation as failed err := repo.UpdateFollowerValidationFailure(testIRI) if err != nil { t.Fatalf("Error updating follower validation failure: %s", err) } // Verify the FirstValidationFailureAt is set followers, err := repo.GetFollowersToValidate(10) if err != nil { t.Fatalf("Error getting followers: %s", err) } found := false for _, f := range followers { if f.ActorIRI == testIRI { found = true if !f.FirstValidationFailureAt.Valid { t.Error("Expected FirstValidationFailureAt to be set after failure") } } } if !found { t.Error("Test follower not found in results") } } func TestValidationFailureClearedOnSuccess(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testuser" createTestFollower(repo, testIRI, "https://fake.server/user/testuser/inbox", "Test User", "testuser") // Mark validation as failed first err := repo.UpdateFollowerValidationFailure(testIRI) if err != nil { t.Fatalf("Error updating follower validation failure: %s", err) } // Then mark as successful err = repo.UpdateFollowerValidationSuccess(testIRI) if err != nil { t.Fatalf("Error updating follower validation success: %s", err) } // Verify FirstValidationFailureAt is cleared followers, err := repo.GetFollowersToValidate(10) if err != nil { t.Fatalf("Error getting followers: %s", err) } for _, f := range followers { if f.ActorIRI == testIRI { if f.FirstValidationFailureAt.Valid { t.Error("Expected FirstValidationFailureAt to be NULL after success") } } } } func TestRemoveByIRI(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testuser" createTestFollower(repo, testIRI, "https://fake.server/user/testuser/inbox", "Test User", "testuser") // Verify follower exists count, err := repo.GetCount() if err != nil { t.Fatalf("Error getting count: %s", err) } if count != 1 { t.Errorf("Expected 1 follower, got %d", count) } // Remove the follower err = repo.RemoveByIRI(testIRI) if err != nil { t.Fatalf("Error removing follower: %s", err) } // Verify follower is removed count, err = repo.GetCount() if err != nil { t.Fatalf("Error getting count: %s", err) } if count != 0 { t.Errorf("Expected 0 followers after removal, got %d", count) } } func TestFailureDurationThresholdLogic(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testfailure" createTestFollower(repo, testIRI, "https://fake.server/user/testfailure/inbox", "Test User", "testfailure") // Set first_validation_failure_at to 8 days ago (past threshold) eightDaysAgo := time.Now().Add(-8 * 24 * time.Hour) _, err := datastore.DB.Exec( "UPDATE ap_followers SET first_validation_failure_at = ? WHERE iri = ?", eightDaysAgo, testIRI, ) if err != nil { t.Fatalf("Error setting first_validation_failure_at: %s", err) } // Fetch the follower followers, err := repo.GetFollowersToValidate(1) if err != nil { t.Fatalf("Error getting followers: %s", err) } if len(followers) != 1 { t.Fatalf("Expected 1 follower, got %d", len(followers)) } // Verify the failure duration is calculated correctly failureDuration := time.Since(followers[0].FirstValidationFailureAt.Time) if failureDuration < FailureDurationThreshold { t.Errorf("Expected failure duration (%v) to be >= threshold (%v)", failureDuration, FailureDurationThreshold) } // Test removal - this directly tests the removal logic without network calls err = repo.RemoveByIRI(testIRI) if err != nil { t.Fatalf("Error removing follower: %s", err) } // Verify the follower was removed count, err := repo.GetCount() if err != nil { t.Fatalf("Error getting count: %s", err) } if count != 0 { t.Errorf("Expected follower to be removed, but count is %d", count) } } func TestFailureNotRemovedBeforeThreshold(t *testing.T) { repo := setupTestWithRepo(t) // Create a test follower testIRI := "https://fake.server/user/testnotremoved" createTestFollower(repo, testIRI, "https://fake.server/user/testnotremoved/inbox", "Test User", "testnotremoved") // Set first_validation_failure_at to 1 day ago (not past threshold) oneDayAgo := time.Now().Add(-1 * 24 * time.Hour) _, err := datastore.DB.Exec( "UPDATE ap_followers SET first_validation_failure_at = ? WHERE iri = ?", oneDayAgo, testIRI, ) if err != nil { t.Fatalf("Error setting first_validation_failure_at: %s", err) } // Fetch the follower followers, err := repo.GetFollowersToValidate(1) if err != nil { t.Fatalf("Error getting followers: %s", err) } if len(followers) != 1 { t.Fatalf("Expected 1 follower, got %d", len(followers)) } // Verify the failure duration is below threshold failureDuration := time.Since(followers[0].FirstValidationFailureAt.Time) if failureDuration >= FailureDurationThreshold { t.Errorf("Expected failure duration (%v) to be < threshold (%v)", failureDuration, FailureDurationThreshold) } // Follower should NOT be removed yet count, err := repo.GetCount() if err != nil { t.Fatalf("Error getting count: %s", err) } if count != 1 { t.Errorf("Expected follower to NOT be removed yet, but count is %d", count) } } func TestFollowersOrderedByOldestValidatedFirst(t *testing.T) { repo := setupTestWithRepo(t) // Create followers iri1 := "https://fake.server/user/first" iri2 := "https://fake.server/user/second" iri3 := "https://fake.server/user/third" createTestFollower(repo, iri1, "https://fake.server/user/first/inbox", "First", "first") createTestFollower(repo, iri2, "https://fake.server/user/second/inbox", "Second", "second") createTestFollower(repo, iri3, "https://fake.server/user/third/inbox", "Third", "third") // Set different last_validated_at times now := time.Now() _, err := datastore.DB.Exec("UPDATE ap_followers SET last_validated_at = ? WHERE iri = ?", now.Add(-3*time.Hour), iri1) if err != nil { t.Fatalf("Error updating: %s", err) } _, err = datastore.DB.Exec("UPDATE ap_followers SET last_validated_at = ? WHERE iri = ?", now.Add(-1*time.Hour), iri2) if err != nil { t.Fatalf("Error updating: %s", err) } // iri3 has NULL last_validated_at (never validated) // Get followers - should return in order: iri3 (NULL), iri1 (oldest), iri2 (newest) followers, err := repo.GetFollowersToValidate(3) if err != nil { t.Fatalf("Error getting followers: %s", err) } if len(followers) != 3 { t.Fatalf("Expected 3 followers, got %d", len(followers)) } // NULL values should come first (NULLS FIRST in query) if followers[0].ActorIRI != iri3 { t.Errorf("Expected first follower to be %s (never validated), got %s", iri3, followers[0].ActorIRI) } // Then oldest validated if followers[1].ActorIRI != iri1 { t.Errorf("Expected second follower to be %s (oldest validated), got %s", iri1, followers[1].ActorIRI) } // Then newest validated if followers[2].ActorIRI != iri2 { t.Errorf("Expected third follower to be %s (newest validated), got %s", iri2, followers[2].ActorIRI) } }