Skip to main content

EducationPub MVP Completion Plan: Phase 5 - Final Verification and End-to-End Validation

Objective:

This final phase ensures the entire system is stable, functional, and meets all MVP success criteria through comprehensive testing and simulation. It's the last step before considering the MVP complete and ready for initial deployment.

Prerequisites:

  • Completion of Phase 1: Foundational Corrections and Data Model Refinement.
  • Completion of Phase 2: Core Module Repository Setup.
  • Completion of Phase 3: Implement Missing Core Endpoints and Dereferencing.
  • Completion of Phase 4: Architectural Refactoring of Module Dependencies.

Tasks:

Task 5.1: Refine AppService.getInboxCollection Query Logic

  • Problem: The getInboxCollection query in AppService currently fetches activities sent by the actor, rather than activities received by them, due to an incorrect where clause. This results in an inaccurate representation of a local actor's inbox.

  • Location: src/core/services/app.service.ts

  • Action:

    1. Open the file src/core/services/app.service.ts.
    2. Locate the getInboxCollection method.
    3. Update the where clause of the activityRepository.findAndCount call to filter by the new recipientActivityPubId column, which was added in Phase 1.2.
       import { Injectable, NotFoundException, Inject } from '@nestjs/common';  
    import { ConfigService } from '@nestjs/config';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository, IsNull, In } from 'typeorm';
    import { LoggerService } from '../../shared/services/logger.service';
    import { normalizeUrl } from '../../shared/utils/url-normalizer';
    import { ActorEntity } from '../../features/activitypub/entities/actor.entity';
    import { ActivityEntity } from '../../features/activitypub/entities/activity.entity';
    import { ContentObjectEntity } from '../../features/activitypub/entities/content-object.entity';
    import { FlashcardEntity } from '../../features/educationpub/entities/flashcard.entity';
    import { FlashcardModelEntity } from '../../features/educationpub/entities/flashcard-model.entity';
    import { FollowEntity } from '../../features/activitypub/entities/follow.entity';
    import { LikeEntity } from '../../features/activitypub/entities/like.entity';
    import { REDIS_CLIENT, RedisClient } from '../redis.module';

    @Injectable()
    export class AppService {
    private readonly instanceBaseUrl: string;

    constructor(
    private readonly configService: ConfigService,
    private readonly logger: LoggerService,
    @InjectRepository(ActorEntity)
    private readonly actorRepository: Repository<ActorEntity>,
    @InjectRepository(ActivityEntity)
    private readonly activityRepository: Repository<ActivityEntity>,
    @InjectRepository(ContentObjectEntity)
    private readonly contentObjectRepository: Repository<ContentObjectEntity>,
    @InjectRepository(FollowEntity)
    private readonly followRepository: Repository<FollowEntity>,
    @InjectRepository(LikeEntity)
    private readonly likeRepository: Repository<LikeEntity>,
    @InjectRepository(FlashcardEntity)
    private readonly flashcardRepository: Repository<FlashcardEntity>,
    @InjectRepository(FlashcardModelEntity)
    private readonly flashcardModelRepository: Repository<FlashcardModelEntity>,
    @Inject(REDIS_CLIENT) private readonly redisClient: RedisClient,
    ) {
    this.logger.setContext(AppService.name);
    this.instanceBaseUrl = this.configService.get<string>('INSTANCE_BASE_URL');
    }

    // ... (existing methods like getActorProfile, getPublicTimeline, getCreatedFlashcardsCollection, getLocalContentObject) ...

    /**
    * Retrieves a paginated collection of activities received by a given local actor (their inbox).
    * @param username The preferred username of the local actor.
    * @param page The page number for pagination.
    * @param perPage The number of items per page.
    * @returns An ActivityPub OrderedCollection page representing the actor's inbox.
    */
    async getInboxCollection(username: string, page: number, perPage: number): Promise<any> {
    this.logger.debug(`Fetching inbox for actor: $&lbrace;username&rcub;, page: ${page}, perPage: ${perPage}`);
    const actor = await this.actorRepository.findOne({ where: { preferredUsername: username } });
    if (!actor) {
    throw new NotFoundException(`Actor with username '$&lbrace;username&rcub;' not found.`);
    }

    // CRITICAL FIX: Filter by recipientActivityPubId, not actorActivityPubId
    const [activities, totalItems] = await this.activityRepository.findAndCount({
    where: { recipientActivityPubId: normalizeUrl(actor.activityPubId) }, // CORRECTED FILTER
    skip: (page - 1) * perPage,
    take: perPage,
    order: { createdAt: 'DESC' }, // Order by newest first
    });

    const items = activities.map(act => act.activityPubId); // Return the IDs of the activities

    // Construct collection URLs
    const collectionId = `${actor.activityPubId}/inbox`;
    const currentPageId = `${collectionId}?page=${page}&perPage=${perPage}`;
    const firstPageId = `${collectionId}?page=1&perPage=${perPage}`;
    const totalPages = Math.ceil(totalItems / perPage);
    const lastPageId = `${collectionId}?page=${totalPages}&perPage=${perPage}`;
    const prevPageId = page > 1 ? `${collectionId}?page=${page - 1}&perPage=${perPage}` : undefined;
    const nextPageId = page < totalPages ? `${collectionId}?page=${page + 1}&perPage=${perPage}` : undefined;

    return {
    '@context': 'https://www.w3.org/ns/activitystreams',
    id: collectionId,
    type: 'OrderedCollectionPage',
    totalItems: totalItems,
    partOf: collectionId,
    first: firstPageId,
    last: lastPageId,
    prev: prevPageId,
    next: nextPageId,
    current: currentPageId,
    orderedItems: items,
    };
    }

    // ... (other methods) ...
    }
  • Expected Outcome:

    • The application will compile and run.
    • Simulated incoming activities directed at a local actor will be correctly processed and saved with the recipientActivityPubId.
    • An authenticated request to /api/actors/{username}/inbox for that local actor will now correctly list the activities received by that specific actor, ensuring accurate inbox functionality.

Task 5.2: Comprehensive Code Review and Logging Check

  • Objective: Ensure all new and modified code paths are adequately logged, and no obvious inconsistencies or potential issues remain. This is a crucial step for maintainability, debugging, and operational visibility.
  • Action:
    1. Review New Code Paths: Conduct a thorough internal review of all code added or modified across Phase 1, 2, 3, and 4. Pay close attention to:
      • src/features/educationpub/views/flashcard.view.ts (typo fix)
      • src/features/activitypub/entities/activity.entity.ts (new column)
      • src/features/activitypub/services/inbox.processor.ts (actor service injection, new column usage)
      • src/app.module.ts (entity registration cleanup)
      • src/core/core.module.ts (TypeORM forFeature setup, guard provision/export)
      • src/features/activitypub/controllers/activitypub.controller.ts (new endpoint)
      • src/core/services/app.service.ts (new getCreatedFlashcardsCollection, getLocalContentObject modifications, getPublicTimeline refinements, getInboxCollection fix, new repository injections)
      • src/features/activitypub/services/actor.service.ts (optional enhancement for actor profile)
      • All module files (*.module.ts) for correct imports after CommonModule refactoring.
      • All controllers for correct @UseGuards() import paths.
    2. Logger Usage: Confirm that LoggerService is appropriately injected and used with this.logger.log, this.logger.warn, this.logger.error, and this.logger.debug for significant events (start/end of operations, successes, failures, warnings). Ensure setContext is used for clarity in each service/controller.
    3. Error Handling: Verify try-catch blocks are present for asynchronous operations, especially external calls (HTTP requests, database operations), and that meaningful exceptions are thrown or logged to prevent silent failures.
    4. Normalization: Double-check that normalizeUrl is consistently applied to all ActivityPub IDs and URIs before storage or comparison to maintain data consistency.
    5. DTO Validation: Reconfirm that ValidationPipe is globally applied in src/main.ts and that DTOs for incoming payloads (e.g., in Activity decorator usage) have appropriate class-validator decorators to ensure data integrity at the API boundary.
  • Expected Outcome: The codebase will be clean, well-documented with internal logging, and resilient to common errors.

Task 5.3: Functional MVP Simulation (End-to-End)

  • Objective: Perform a final, integrated simulation of the MVP's core functionalities to validate end-to-end flows and confirm all success criteria are met. This step simulates real-world usage scenarios.
  • Action (Simulated Execution Flow):
    1. User Registration & Login:
      • Execute the user registration endpoint (POST /auth/register). Verify a new user and associated actor are created.
      • Execute the user login endpoint (POST /auth/login). Verify a valid JWT token is returned.
    2. Local Actor & Profile Access:
      • Access the newly registered user's actor profile (GET /api/actors/{username}). Verify the profile is returned correctly and includes the edu:flashcards collection URL.
    3. Flashcard Creation (Public):
      • Create a new edu:FlashcardModel via the appropriate API endpoint (e.g., POST /edu/flashcard-models).
      • Create a public edu:Flashcard instance linked to the model via the appropriate API endpoint (e.g., POST /edu/flashcards).
    4. Verify Outbox Processing:
      • Monitor internal logs/queues (outbox-queue) to confirm the "Create" activities for both FlashcardModel and Flashcard are enqueued, processed by OutboxProcessor, and simulated as delivered to followers.
    5. Access Created Flashcards Collection:
      • Access the new /api/actors/{username}/flashcards endpoint. Verify the newly created public flashcard appears in the paginated collection.
    6. Simulate Remote Follow & Inbox:
      • Simulate an external ActivityPub actor sending a "Follow" activity to the local actor's inbox (POST /api/actors/{username}/inbox).
      • Verify that the incoming "Follow" activity is correctly processed by HttpSignatureVerificationGuard and Activity decorator.
      • Confirm FollowHandler correctly updates the FollowEntity in the database and dispatches an "Accept" activity back via the outbox.
    7. Access Inbox Collection (Authenticated):
      • As the local actor who received the follow, access their inbox (GET /api/actors/{username}/inbox). Verify the "Follow" activity (or its internal representation) is correctly listed.
    8. Flashcard Like/Boost:
      • Simulate a local user "Liking" an existing edu:Flashcard (e.g., via POST /api/actors/{username}/outbox with a "Like" activity).
      • Verify the "Like" activity is enqueued and processed by the outbox.
      • (Optional, if implemented) Verify the likesCount on the FlashcardEntity is incremented.
    9. Object Dereferencing:
      • Retrieve the edu:Flashcard object by its activityPubId (GET /api/objects/{flashcardId}). Verify the correct JSON-LD representation is returned.
      • Retrieve the edu:FlashcardModel object by its activityPubId (GET /api/objects/{flashcardModelId}). Verify the correct JSON-LD representation is returned.
  • Expected Outcome: All core MVP functionalities will be validated end-to-end, demonstrating a stable, interoperable, and complete Minimum Viable Product.

Conclusion:

Upon successful completion of all tasks in Phase 5, the EducationPub MVP will be considered complete, ready for initial testing and feedback from target users.

Future Improvements (Beyond MVP):

These are areas identified for future development, beyond the scope of this MVP:

  • Public Timeline Optimization: Enhance AppService.getPublicTimeline for broader content types and improved performance.
  • Private Key Management: Implement secure storage for private keys using a dedicated Key Management System (KMS).
  • Advanced Social Features: Introduce mentions, replies, groups, direct messages, and advanced content discovery.
  • Real-time Notifications: Implement active notification delivery mechanisms (e.g., push notifications, websockets).