diff --git a/plugins/knowledgebase/__init__.py b/plugins/knowledgebase/__init__.py new file mode 100644 index 0000000..b4a73f0 --- /dev/null +++ b/plugins/knowledgebase/__init__.py @@ -0,0 +1,5 @@ +"""Knowledge Base plugin package.""" + +from .plugin import KnowledgeBasePlugin + +__all__ = ['KnowledgeBasePlugin'] diff --git a/plugins/knowledgebase/api/__init__.py b/plugins/knowledgebase/api/__init__.py new file mode 100644 index 0000000..afcaaca --- /dev/null +++ b/plugins/knowledgebase/api/__init__.py @@ -0,0 +1,5 @@ +"""Knowledge Base plugin API.""" + +from .routes import knowledgebase_bp + +__all__ = ['knowledgebase_bp'] diff --git a/shopdb/core/api/knowledgebase.py b/plugins/knowledgebase/api/routes.py similarity index 93% rename from shopdb/core/api/knowledgebase.py rename to plugins/knowledgebase/api/routes.py index 737f595..94b2eee 100644 --- a/shopdb/core/api/knowledgebase.py +++ b/plugins/knowledgebase/api/routes.py @@ -1,207 +1,210 @@ -"""Knowledge Base API endpoints.""" - -from flask import Blueprint, request -from flask_jwt_extended import jwt_required - -from shopdb.extensions import db -from shopdb.core.models import KnowledgeBase, Application -from shopdb.utils.responses import ( - success_response, - error_response, - paginated_response, - ErrorCodes -) -from shopdb.utils.pagination import get_pagination_params, paginate_query - -knowledgebase_bp = Blueprint('knowledgebase', __name__) - - -@knowledgebase_bp.route('', methods=['GET']) -@jwt_required(optional=True) -def list_articles(): - """List all knowledge base articles.""" - page, per_page = get_pagination_params(request) - - query = KnowledgeBase.query.filter_by(isactive=True) - - # Search - if search := request.args.get('search'): - query = query.filter( - db.or_( - KnowledgeBase.shortdescription.ilike(f'%{search}%'), - KnowledgeBase.keywords.ilike(f'%{search}%') - ) - ) - - # Filter by topic/application - if appid := request.args.get('appid'): - query = query.filter(KnowledgeBase.appid == int(appid)) - - # Sort options - sort = request.args.get('sort', 'clicks') - order = request.args.get('order', 'desc') - - if sort == 'clicks': - query = query.order_by( - KnowledgeBase.clicks.desc() if order == 'desc' else KnowledgeBase.clicks.asc(), - KnowledgeBase.lastupdated.desc() - ) - elif sort == 'topic': - query = query.join(Application).order_by( - Application.appname.desc() if order == 'desc' else Application.appname.asc() - ) - elif sort == 'description': - query = query.order_by( - KnowledgeBase.shortdescription.desc() if order == 'desc' else KnowledgeBase.shortdescription.asc() - ) - elif sort == 'lastupdated': - query = query.order_by( - KnowledgeBase.lastupdated.desc() if order == 'desc' else KnowledgeBase.lastupdated.asc() - ) - else: - query = query.order_by(KnowledgeBase.clicks.desc()) - - items, total = paginate_query(query, page, per_page) - data = [] - for article in items: - article_dict = article.to_dict() - if article.application: - article_dict['application'] = { - 'appid': article.application.appid, - 'appname': article.application.appname - } - else: - article_dict['application'] = None - data.append(article_dict) - - return paginated_response(data, page, per_page, total) - - -@knowledgebase_bp.route('/stats', methods=['GET']) -@jwt_required(optional=True) -def get_stats(): - """Get knowledge base statistics.""" - total_clicks = db.session.query( - db.func.coalesce(db.func.sum(KnowledgeBase.clicks), 0) - ).filter(KnowledgeBase.isactive == True).scalar() - - total_articles = KnowledgeBase.query.filter_by(isactive=True).count() - - return success_response({ - 'totalclicks': int(total_clicks), - 'totalarticles': total_articles - }) - - -@knowledgebase_bp.route('/', methods=['GET']) -@jwt_required(optional=True) -def get_article(link_id: int): - """Get a single knowledge base article.""" - article = KnowledgeBase.query.get(link_id) - - if not article or not article.isactive: - return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) - - data = article.to_dict() - if article.application: - data['application'] = { - 'appid': article.application.appid, - 'appname': article.application.appname - } - else: - data['application'] = None - - return success_response(data) - - -@knowledgebase_bp.route('//click', methods=['POST']) -@jwt_required(optional=True) -def track_click(link_id: int): - """Increment click counter and return the URL to redirect to.""" - article = KnowledgeBase.query.get(link_id) - - if not article or not article.isactive: - return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) - - article.increment_clicks() - db.session.commit() - - return success_response({ - 'linkurl': article.linkurl, - 'clicks': article.clicks - }) - - -@knowledgebase_bp.route('', methods=['POST']) -@jwt_required() -def create_article(): - """Create a new knowledge base article.""" - data = request.get_json() - - if not data or not data.get('shortdescription'): - return error_response(ErrorCodes.VALIDATION_ERROR, 'shortdescription is required') - - if not data.get('linkurl'): - return error_response(ErrorCodes.VALIDATION_ERROR, 'linkurl is required') - - # Validate application if provided - if data.get('appid'): - app = Application.query.get(data['appid']) - if not app: - return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) - - article = KnowledgeBase( - shortdescription=data['shortdescription'], - linkurl=data['linkurl'], - appid=data.get('appid'), - keywords=data.get('keywords'), - clicks=0 - ) - - db.session.add(article) - db.session.commit() - - return success_response(article.to_dict(), message='Article created', http_code=201) - - -@knowledgebase_bp.route('/', methods=['PUT']) -@jwt_required() -def update_article(link_id: int): - """Update a knowledge base article.""" - article = KnowledgeBase.query.get(link_id) - - if not article: - return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) - - data = request.get_json() - if not data: - return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') - - # Validate application if being changed - if 'appid' in data and data['appid']: - app = Application.query.get(data['appid']) - if not app: - return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) - - fields = ['shortdescription', 'linkurl', 'appid', 'keywords', 'isactive'] - for key in fields: - if key in data: - setattr(article, key, data[key]) - - db.session.commit() - return success_response(article.to_dict(), message='Article updated') - - -@knowledgebase_bp.route('/', methods=['DELETE']) -@jwt_required() -def delete_article(link_id: int): - """Delete (deactivate) a knowledge base article.""" - article = KnowledgeBase.query.get(link_id) - - if not article: - return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) - - article.isactive = False - db.session.commit() - - return success_response(message='Article deleted') +"""Knowledge Base API endpoints.""" + +from flask import Blueprint, request +from flask_jwt_extended import jwt_required + +from shopdb.api import ( + db, + Application, + success_response, + error_response, + paginated_response, + ErrorCodes, + get_pagination_params, + paginate_query, +) + +from ..models import KnowledgeBase + +knowledgebase_bp = Blueprint('knowledgebase', __name__) + + +@knowledgebase_bp.route('', methods=['GET']) +@jwt_required(optional=True) +def list_articles(): + """List all knowledge base articles.""" + page, per_page = get_pagination_params(request) + + query = KnowledgeBase.query.filter_by(isactive=True) + + # Search + if search := request.args.get('search'): + query = query.filter( + db.or_( + KnowledgeBase.shortdescription.ilike(f'%{search}%'), + KnowledgeBase.keywords.ilike(f'%{search}%') + ) + ) + + # Filter by topic/application + if appid := request.args.get('appid'): + query = query.filter(KnowledgeBase.appid == int(appid)) + + # Sort options + sort = request.args.get('sort', 'clicks') + order = request.args.get('order', 'desc') + + if sort == 'clicks': + query = query.order_by( + KnowledgeBase.clicks.desc() if order == 'desc' else KnowledgeBase.clicks.asc(), + KnowledgeBase.lastupdated.desc() + ) + elif sort == 'topic': + query = query.join(Application).order_by( + Application.appname.desc() if order == 'desc' else Application.appname.asc() + ) + elif sort == 'description': + query = query.order_by( + KnowledgeBase.shortdescription.desc() if order == 'desc' else KnowledgeBase.shortdescription.asc() + ) + elif sort == 'lastupdated': + query = query.order_by( + KnowledgeBase.lastupdated.desc() if order == 'desc' else KnowledgeBase.lastupdated.asc() + ) + else: + query = query.order_by(KnowledgeBase.clicks.desc()) + + items, total = paginate_query(query, page, per_page) + data = [] + for article in items: + article_dict = article.to_dict() + if article.application: + article_dict['application'] = { + 'appid': article.application.appid, + 'appname': article.application.appname + } + else: + article_dict['application'] = None + data.append(article_dict) + + return paginated_response(data, page, per_page, total) + + +@knowledgebase_bp.route('/stats', methods=['GET']) +@jwt_required(optional=True) +def get_stats(): + """Get knowledge base statistics.""" + total_clicks = db.session.query( + db.func.coalesce(db.func.sum(KnowledgeBase.clicks), 0) + ).filter(KnowledgeBase.isactive == True).scalar() + + total_articles = KnowledgeBase.query.filter_by(isactive=True).count() + + return success_response({ + 'totalclicks': int(total_clicks), + 'totalarticles': total_articles + }) + + +@knowledgebase_bp.route('/', methods=['GET']) +@jwt_required(optional=True) +def get_article(link_id: int): + """Get a single knowledge base article.""" + article = KnowledgeBase.query.get(link_id) + + if not article or not article.isactive: + return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) + + data = article.to_dict() + if article.application: + data['application'] = { + 'appid': article.application.appid, + 'appname': article.application.appname + } + else: + data['application'] = None + + return success_response(data) + + +@knowledgebase_bp.route('//click', methods=['POST']) +@jwt_required(optional=True) +def track_click(link_id: int): + """Increment click counter and return the URL to redirect to.""" + article = KnowledgeBase.query.get(link_id) + + if not article or not article.isactive: + return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) + + article.increment_clicks() + db.session.commit() + + return success_response({ + 'linkurl': article.linkurl, + 'clicks': article.clicks + }) + + +@knowledgebase_bp.route('', methods=['POST']) +@jwt_required() +def create_article(): + """Create a new knowledge base article.""" + data = request.get_json() + + if not data or not data.get('shortdescription'): + return error_response(ErrorCodes.VALIDATION_ERROR, 'shortdescription is required') + + if not data.get('linkurl'): + return error_response(ErrorCodes.VALIDATION_ERROR, 'linkurl is required') + + # Validate application if provided + if data.get('appid'): + app = Application.query.get(data['appid']) + if not app: + return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) + + article = KnowledgeBase( + shortdescription=data['shortdescription'], + linkurl=data['linkurl'], + appid=data.get('appid'), + keywords=data.get('keywords'), + clicks=0 + ) + + db.session.add(article) + db.session.commit() + + return success_response(article.to_dict(), message='Article created', http_code=201) + + +@knowledgebase_bp.route('/', methods=['PUT']) +@jwt_required() +def update_article(link_id: int): + """Update a knowledge base article.""" + article = KnowledgeBase.query.get(link_id) + + if not article: + return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) + + data = request.get_json() + if not data: + return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided') + + # Validate application if being changed + if 'appid' in data and data['appid']: + app = Application.query.get(data['appid']) + if not app: + return error_response(ErrorCodes.NOT_FOUND, 'Application not found', http_code=404) + + fields = ['shortdescription', 'linkurl', 'appid', 'keywords', 'isactive'] + for key in fields: + if key in data: + setattr(article, key, data[key]) + + db.session.commit() + return success_response(article.to_dict(), message='Article updated') + + +@knowledgebase_bp.route('/', methods=['DELETE']) +@jwt_required() +def delete_article(link_id: int): + """Delete (deactivate) a knowledge base article.""" + article = KnowledgeBase.query.get(link_id) + + if not article: + return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404) + + article.isactive = False + db.session.commit() + + return success_response(message='Article deleted') diff --git a/plugins/knowledgebase/manifest.json b/plugins/knowledgebase/manifest.json new file mode 100644 index 0000000..3a34973 --- /dev/null +++ b/plugins/knowledgebase/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "knowledgebase", + "version": "1.0.0", + "description": "Knowledge Base articles linking to external resources", + "author": "ShopDB Team", + "dependencies": [], + "core_version": ">=0.1.0,<1.0.0", + "api_prefix": "/api/knowledgebase", + "provides": { + "features": ["knowledgebase"] + } +} diff --git a/plugins/knowledgebase/models/__init__.py b/plugins/knowledgebase/models/__init__.py new file mode 100644 index 0000000..761162a --- /dev/null +++ b/plugins/knowledgebase/models/__init__.py @@ -0,0 +1,5 @@ +"""Knowledge Base plugin models.""" + +from .knowledgebase import KnowledgeBase + +__all__ = ['KnowledgeBase'] diff --git a/shopdb/core/models/knowledgebase.py b/plugins/knowledgebase/models/knowledgebase.py similarity index 65% rename from shopdb/core/models/knowledgebase.py rename to plugins/knowledgebase/models/knowledgebase.py index 316b4e9..909d289 100644 --- a/shopdb/core/models/knowledgebase.py +++ b/plugins/knowledgebase/models/knowledgebase.py @@ -1,27 +1,33 @@ -"""Knowledge Base models.""" - -from shopdb.extensions import db -from .base import BaseModel - - -class KnowledgeBase(BaseModel): - """Knowledge Base article linking to external resources.""" - __tablename__ = 'knowledgebase' - - linkid = db.Column(db.Integer, primary_key=True) - appid = db.Column(db.Integer, db.ForeignKey('applications.appid')) - shortdescription = db.Column(db.String(500), nullable=False) - linkurl = db.Column(db.String(2000)) - keywords = db.Column(db.String(500)) - clicks = db.Column(db.Integer, default=0) - lastupdated = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now()) - - # Relationships - application = db.relationship('Application', backref=db.backref('knowledgebase_articles', lazy='dynamic')) - - def __repr__(self): - return f"" - - def increment_clicks(self): - """Increment click counter.""" - self.clicks = (self.clicks or 0) + 1 +"""Knowledge Base model. + +Non-asset plugin model. The `knowledgebase` table lives in the core Alembic +chain (bundled-plugin schema is folded into core, ADR-004); this class just +maps it and is registered via the plugin's get_models hook. The appid FK +references the core applications table by name, which resolves at mapper config +time without importing the core model. +""" + +from shopdb.api import db, BaseModel + + +class KnowledgeBase(BaseModel): + """Knowledge Base article linking to external resources.""" + __tablename__ = 'knowledgebase' + + linkid = db.Column(db.Integer, primary_key=True) + appid = db.Column(db.Integer, db.ForeignKey('applications.appid')) + shortdescription = db.Column(db.String(500), nullable=False) + linkurl = db.Column(db.String(2000)) + keywords = db.Column(db.String(500)) + clicks = db.Column(db.Integer, default=0) + lastupdated = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now()) + + # Relationship to the core Application model (resolved by class name). + application = db.relationship('Application', backref=db.backref('knowledgebase_articles', lazy='dynamic')) + + def __repr__(self): + return f"" + + def increment_clicks(self): + """Increment click counter.""" + self.clicks = (self.clicks or 0) + 1 diff --git a/plugins/knowledgebase/plugin.py b/plugins/knowledgebase/plugin.py new file mode 100644 index 0000000..2edb932 --- /dev/null +++ b/plugins/knowledgebase/plugin.py @@ -0,0 +1,74 @@ +"""Knowledge Base plugin main class. + +Reference NON-asset plugin: contributes a model + blueprint + nav item but no +AssetType (it is not an asset). Mirrors the notifications plugin. +""" + +import json +import logging +from pathlib import Path +from typing import List, Dict, Optional, Type + +from flask import Flask, Blueprint + +from shopdb.plugins.base import BasePlugin, PluginMeta + +from .models import KnowledgeBase +from .api import knowledgebase_bp + +logger = logging.getLogger(__name__) + + +class KnowledgeBasePlugin(BasePlugin): + """Knowledge Base plugin - articles linking to external resources.""" + + def __init__(self): + self._manifest = self._load_manifest() + + def _load_manifest(self) -> Dict: + """Load plugin manifest from JSON file.""" + manifest_path = Path(__file__).parent / 'manifest.json' + if manifest_path.exists(): + with open(manifest_path, 'r') as f: + return json.load(f) + return {} + + @property + def meta(self) -> PluginMeta: + """Return plugin metadata.""" + return PluginMeta( + name=self._manifest.get('name', 'knowledgebase'), + version=self._manifest.get('version', '1.0.0'), + description=self._manifest.get( + 'description', + 'Knowledge Base articles linking to external resources' + ), + author=self._manifest.get('author', 'ShopDB Team'), + dependencies=self._manifest.get('dependencies', []), + core_version=self._manifest.get('core_version', '>=0.1.0,<1.0.0'), + api_prefix=self._manifest.get('api_prefix', '/api/knowledgebase'), + ) + + def get_blueprint(self) -> Optional[Blueprint]: + """Return Flask Blueprint with API routes.""" + return knowledgebase_bp + + def get_models(self) -> List[Type]: + """Return list of SQLAlchemy model classes.""" + return [KnowledgeBase] + + def init_app(self, app: Flask, db_instance) -> None: + """Initialize plugin with Flask app.""" + logger.info(f"Knowledge Base plugin initialized (v{self.meta.version})") + + def get_navigation_items(self) -> List[Dict]: + """Return navigation menu items.""" + return [ + { + 'name': 'Knowledge Base', + 'icon': 'book-open', + 'route': '/knowledgebase', + 'position': 35, + 'section': 'information', + }, + ] diff --git a/shopdb/__init__.py b/shopdb/__init__.py index 71b1a77..89daaff 100644 --- a/shopdb/__init__.py +++ b/shopdb/__init__.py @@ -102,7 +102,6 @@ CORE_BLUEPRINT_NAMES = ( 'operatingsystems', 'dashboard', 'applications', - 'knowledgebase', 'search', 'reports', 'collector', diff --git a/shopdb/core/api/__init__.py b/shopdb/core/api/__init__.py index f6d9385..8b55dd3 100644 --- a/shopdb/core/api/__init__.py +++ b/shopdb/core/api/__init__.py @@ -11,7 +11,6 @@ from .locations import locations_bp from .operatingsystems import operatingsystems_bp from .dashboard import dashboard_bp from .applications import applications_bp -from .knowledgebase import knowledgebase_bp from .search import search_bp from .reports import reports_bp from .collector import collector_bp @@ -33,7 +32,6 @@ __all__ = [ 'operatingsystems_bp', 'dashboard_bp', 'applications_bp', - 'knowledgebase_bp', 'search_bp', 'reports_bp', 'collector_bp', diff --git a/shopdb/core/api/dashboard.py b/shopdb/core/api/dashboard.py index 3dab775..eac3f3d 100644 --- a/shopdb/core/api/dashboard.py +++ b/shopdb/core/api/dashboard.py @@ -142,7 +142,6 @@ def get_navigation(): # Add core information section items all_items.extend([ {'name': 'Applications', 'icon': 'app-window', 'route': '/applications', 'position': 30, 'section': 'information'}, - {'name': 'Knowledge Base', 'icon': 'book-open', 'route': '/knowledgebase', 'position': 35, 'section': 'information'}, {'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports', 'position': 40, 'section': 'information'}, ]) diff --git a/shopdb/core/api/reports.py b/shopdb/core/api/reports.py index 5d64a54..ae5bf51 100644 --- a/shopdb/core/api/reports.py +++ b/shopdb/core/api/reports.py @@ -9,7 +9,7 @@ from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import ( Asset, AssetType, AssetStatus, - Application, KnowledgeBase + Application ) from shopdb.utils.responses import success_response, error_response, ErrorCodes @@ -165,6 +165,15 @@ def kb_popularity(): """ limit = min(int(request.args.get('limit', 20)), 100) + # Knowledge Base is a plugin; degrade gracefully if it is not installed. + try: + from plugins.knowledgebase.models import KnowledgeBase + except ImportError: + return error_response( + ErrorCodes.INTERNAL_ERROR, + 'KB popularity requires the knowledgebase plugin', + http_code=503) + articles = KnowledgeBase.query.filter( KnowledgeBase.isactive == True ).order_by( diff --git a/shopdb/core/api/search.py b/shopdb/core/api/search.py index 019962f..62e2097 100644 --- a/shopdb/core/api/search.py +++ b/shopdb/core/api/search.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import joinedload from shopdb.extensions import db from shopdb.core.models import ( - Application, KnowledgeBase, + Application, Asset, AssetType, Communication, Vendor, Model ) from shopdb.utils.responses import success_response @@ -131,6 +131,8 @@ def _search_knowledgebase(query, search_term): """Search Knowledge Base by description and keywords.""" results = [] try: + _require_enabled('knowledgebase') + from plugins.knowledgebase.models import KnowledgeBase kb_articles = KnowledgeBase.query.filter( KnowledgeBase.isactive == True, db.or_( @@ -153,6 +155,8 @@ def _search_knowledgebase(query, search_term): 'linkurl': kb.linkurl, 'relevance': relevance }) + except ImportError: + pass # knowledgebase plugin absent or disabled except Exception as e: logger.error(f"KnowledgeBase search failed: {e}") return results diff --git a/shopdb/core/models/__init__.py b/shopdb/core/models/__init__.py index 4ffd337..4d2518f 100644 --- a/shopdb/core/models/__init__.py +++ b/shopdb/core/models/__init__.py @@ -12,7 +12,6 @@ from .relationship import AssetRelationship, RelationshipType from .communication import Communication, CommunicationType from .user import User, Role, Permission from .application import Application, AppVersion, AppOwner, SupportTeam -from .knowledgebase import KnowledgeBase from .setting import Setting from .auditlog import AuditLog @@ -50,7 +49,6 @@ __all__ = [ 'AppOwner', 'SupportTeam', # Knowledge Base - 'KnowledgeBase', # Settings 'Setting', # Audit diff --git a/tests/test_core/test_knowledgebase.py b/tests/test_core/test_knowledgebase.py new file mode 100644 index 0000000..6cb2c20 --- /dev/null +++ b/tests/test_core/test_knowledgebase.py @@ -0,0 +1,34 @@ +"""Characterization tests for the Knowledge Base API. + +Written before extracting Knowledge Base into a plugin (pinning-flask-behavior): +the endpoints must behave identically whether KB is a core blueprint or a plugin +blueprint, since both register at /api/knowledgebase. +""" + + +def test_create_list_get_article(client, db, auth_headers): + """Create an article, see it in the list, fetch it by id.""" + created = client.post('/api/knowledgebase', + json={'shortdescription': 'How to reset a printer', + 'linkurl': 'https://kb.example/printer-reset', + 'keywords': 'printer reset'}, + headers=auth_headers) + assert created.status_code == 201, created.get_json() + linkid = created.get_json()['data']['linkid'] + + listing = client.get('/api/knowledgebase', headers=auth_headers) + assert listing.status_code == 200 + ids = [a['linkid'] for a in listing.get_json()['data']] + assert linkid in ids + + fetched = client.get(f'/api/knowledgebase/{linkid}', headers=auth_headers) + assert fetched.status_code == 200 + assert fetched.get_json()['data']['shortdescription'] == 'How to reset a printer' + + +def test_create_requires_shortdescription(client, db, auth_headers): + """shortdescription is required.""" + resp = client.post('/api/knowledgebase', + json={'linkurl': 'https://kb.example/x'}, + headers=auth_headers) + assert resp.status_code == 400 diff --git a/tests/test_plugin_contract.py b/tests/test_plugin_contract.py index 41fd0c3..3d37732 100644 --- a/tests/test_plugin_contract.py +++ b/tests/test_plugin_contract.py @@ -17,7 +17,7 @@ from shopdb.plugins import plugin_manager from shopdb.plugins.base import BasePlugin, PluginMeta -BUNDLED_PLUGINS = ('computers', 'equipment', 'network', 'notifications', 'printers', 'usb') +BUNDLED_PLUGINS = ('computers', 'equipment', 'knowledgebase', 'network', 'notifications', 'printers', 'usb') @pytest.fixture diff --git a/tests/test_smoke.py b/tests/test_smoke.py index d67613d..392e4bf 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -89,12 +89,13 @@ def test_paginated_response_shape(client, auth_headers): def test_plugin_loader_discovers_bundled_plugins(app): - """Plugin manager finds the six bundled plugins.""" + """Plugin manager finds the bundled plugins.""" from shopdb.plugins import plugin_manager expected_plugins = { 'computers', 'equipment', + 'knowledgebase', 'network', 'notifications', 'printers',