Add license and protect repository #2
62 changed files with 1237 additions and 132 deletions
35
.github/CODEOWNERS
vendored
Normal file
35
.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Code Owners for GeoGuessr MCP Server
|
||||||
|
#
|
||||||
|
# This file defines individuals or teams responsible for code in this repository.
|
||||||
|
# Code owners are automatically requested for review when someone opens a pull request
|
||||||
|
# that modifies code that they own.
|
||||||
|
#
|
||||||
|
# More info: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
|
# Default owner for everything in the repo
|
||||||
|
* @NyxiumYuuki
|
||||||
|
|
||||||
|
# Core source code
|
||||||
|
/src/ @NyxiumYuuki
|
||||||
|
|
||||||
|
# API and Authentication
|
||||||
|
/src/geoguessr_mcp/api/ @NyxiumYuuki
|
||||||
|
/src/geoguessr_mcp/auth/ @NyxiumYuuki
|
||||||
|
|
||||||
|
# Monitoring system
|
||||||
|
/src/geoguessr_mcp/monitoring/ @NyxiumYuuki
|
||||||
|
|
||||||
|
# Configuration files
|
||||||
|
/pyproject.toml @NyxiumYuuki
|
||||||
|
/docker-compose*.yml @NyxiumYuuki
|
||||||
|
/Dockerfile @NyxiumYuuki
|
||||||
|
|
||||||
|
# Security and policies
|
||||||
|
/SECURITY.md @NyxiumYuuki
|
||||||
|
/LICENSE @NyxiumYuuki
|
||||||
|
|
||||||
|
# CI/CD and GitHub workflows
|
||||||
|
/.github/ @NyxiumYuuki
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/tests/ @NyxiumYuuki
|
||||||
66
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what the bug is -->
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
4.
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what you expected to happen -->
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what actually happened -->
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- **OS**: <!-- e.g., Ubuntu 22.04, macOS 14.0, Windows 11 -->
|
||||||
|
- **Python Version**: <!-- e.g., 3.13.0 -->
|
||||||
|
- **GeoGuessr MCP Version**: <!-- e.g., 0.1.0 -->
|
||||||
|
- **Deployment Method**: <!-- Docker, Local, etc. -->
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
<!-- Relevant configuration (remove sensitive information) -->
|
||||||
|
|
||||||
|
```env
|
||||||
|
MONITORING_ENABLED=true
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
<!-- Paste relevant log output here -->
|
||||||
|
|
||||||
|
```
|
||||||
|
[Paste logs here]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
<!-- If applicable, add screenshots to help explain your problem -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Add any other context about the problem here -->
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
|
||||||
|
<!-- If you have suggestions on how to fix the bug, describe them here -->
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
<!-- Link to any related issues -->
|
||||||
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
|
||||||
|
<!-- A clear and concise description of the feature you'd like to see -->
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
<!-- Describe the problem this feature would solve -->
|
||||||
|
<!-- Example: I'm always frustrated when... -->
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
<!-- A clear and concise description of what you want to happen -->
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
<!-- Describe any alternative solutions or features you've considered -->
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
<!-- Describe specific use cases for this feature -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
<!-- Show how you envision using this feature -->
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Example code showing the proposed feature
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
<!-- What benefits would this feature provide? -->
|
||||||
|
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
## Potential Drawbacks
|
||||||
|
|
||||||
|
<!-- Are there any potential downsides or challenges? -->
|
||||||
|
|
||||||
|
## Implementation Suggestions
|
||||||
|
|
||||||
|
<!-- If you have ideas about how to implement this, share them here -->
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Add any other context, screenshots, or mockups about the feature request here -->
|
||||||
|
|
||||||
|
## Priority
|
||||||
|
|
||||||
|
<!-- How important is this feature to you? -->
|
||||||
|
|
||||||
|
- [ ] Critical - Blocking my use of the project
|
||||||
|
- [ ] High - Would significantly improve my workflow
|
||||||
|
- [ ] Medium - Nice to have
|
||||||
|
- [ ] Low - Just an idea
|
||||||
|
|
||||||
|
## Willingness to Contribute
|
||||||
|
|
||||||
|
<!-- Are you willing to contribute to implementing this feature? -->
|
||||||
|
|
||||||
|
- [ ] I can implement this feature
|
||||||
|
- [ ] I can help with implementation
|
||||||
|
- [ ] I can test the implementation
|
||||||
|
- [ ] I can only report the idea
|
||||||
83
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
83
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Pull Request
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- Provide a clear and concise description of your changes -->
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
<!-- Mark the relevant option with an 'x' -->
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] Documentation update
|
||||||
|
- [ ] Code refactoring
|
||||||
|
- [ ] Performance improvement
|
||||||
|
- [ ] Test improvement
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
<!-- Link related issues using keywords: Fixes #123, Closes #456, Related to #789 -->
|
||||||
|
|
||||||
|
Fixes #
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
<!-- List the main changes in bullet points -->
|
||||||
|
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
## Testing Performed
|
||||||
|
|
||||||
|
<!-- Describe the testing you've done -->
|
||||||
|
|
||||||
|
- [ ] Unit tests added/updated
|
||||||
|
- [ ] Integration tests added/updated
|
||||||
|
- [ ] Manual testing performed
|
||||||
|
- [ ] All existing tests pass
|
||||||
|
|
||||||
|
### Test Details
|
||||||
|
|
||||||
|
<!-- Provide details about how you tested your changes -->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example test commands
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots (if applicable)
|
||||||
|
|
||||||
|
<!-- Add screenshots to help explain your changes -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!-- Ensure all items are completed before submitting -->
|
||||||
|
|
||||||
|
- [ ] My code follows the project's style guidelines
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have made corresponding changes to the documentation
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] New and existing unit tests pass locally with my changes
|
||||||
|
- [ ] Any dependent changes have been merged and published
|
||||||
|
- [ ] I have checked my code and corrected any misspellings
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
<!-- Add any other context about the pull request here -->
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
<!-- If this is a breaking change, describe the impact and migration path -->
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
<!-- Describe any performance implications of your changes -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**By submitting this pull request, I confirm that my contribution is made under the terms of the MIT License.**
|
||||||
123
.github/workflows/code-quality.yml
vendored
Normal file
123
.github/workflows/code-quality.yml
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
name: Code Quality
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop, claude/** ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-format:
|
||||||
|
name: Lint and Format Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install black ruff mypy
|
||||||
|
|
||||||
|
- name: Check formatting with Black
|
||||||
|
run: |
|
||||||
|
black --check src/
|
||||||
|
|
||||||
|
- name: Lint with Ruff
|
||||||
|
run: |
|
||||||
|
ruff check src/
|
||||||
|
|
||||||
|
- name: Type check with MyPy
|
||||||
|
run: |
|
||||||
|
mypy src/
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Run Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ['3.13']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: |
|
||||||
|
pytest src/tests/ -v --cov=src/geoguessr_mcp --cov-report=xml --cov-report=term
|
||||||
|
|
||||||
|
- name: Upload coverage reports
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
security:
|
||||||
|
name: Security Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install safety bandit
|
||||||
|
|
||||||
|
- name: Run Safety check
|
||||||
|
run: |
|
||||||
|
pip freeze | safety check --stdin
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run Bandit security scan
|
||||||
|
run: |
|
||||||
|
bandit -r src/ -ll
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Docker Build Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
tags: geoguessr-mcp:test
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
25
.github/workflows/dependency-review.yml
vendored
Normal file
25
.github/workflows/dependency-review.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Dependency Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
name: Review Dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Dependency Review
|
||||||
|
uses: actions/dependency-review-action@v4
|
||||||
|
with:
|
||||||
|
fail-on-severity: moderate
|
||||||
|
deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0
|
||||||
|
comment-summary-in-pr: always
|
||||||
139
CODE_OF_CONDUCT.md
Normal file
139
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying and enforcing our standards
|
||||||
|
of acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the project maintainers responsible for enforcement at:
|
||||||
|
|
||||||
|
**yuki.vachot@datasingularity.fr**
|
||||||
|
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All project maintainers are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Project maintainers will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from project maintainers, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Contact**: yuki.vachot@datasingularity.fr
|
||||||
|
**Last Updated**: 2025-11-29
|
||||||
351
CONTRIBUTING.md
Normal file
351
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
# Contributing to GeoGuessr MCP Server
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to the GeoGuessr MCP Server! This document provides guidelines and instructions for contributing.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Code of Conduct](#code-of-conduct)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Development Setup](#development-setup)
|
||||||
|
- [How to Contribute](#how-to-contribute)
|
||||||
|
- [Coding Standards](#coding-standards)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Pull Request Process](#pull-request-process)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
### Our Standards
|
||||||
|
|
||||||
|
- **Be Respectful**: Treat everyone with respect and professionalism
|
||||||
|
- **Be Collaborative**: Work together constructively
|
||||||
|
- **Be Patient**: Help others learn and grow
|
||||||
|
- **Be Inclusive**: Welcome diverse perspectives and backgrounds
|
||||||
|
|
||||||
|
### Unacceptable Behavior
|
||||||
|
|
||||||
|
- Harassment, discrimination, or offensive comments
|
||||||
|
- Personal attacks or trolling
|
||||||
|
- Publishing others' private information
|
||||||
|
- Any conduct that would be inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Fork the repository** on GitHub
|
||||||
|
2. **Clone your fork** locally:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/GeoGuessrMCP.git
|
||||||
|
cd GeoGuessrMCP
|
||||||
|
```
|
||||||
|
3. **Add upstream remote**:
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/NyxiumYuuki/GeoGuessrMCP.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.13 or higher
|
||||||
|
- Docker and Docker Compose (for containerized development)
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create virtual environment
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Set up pre-commit hooks (optional but recommended)
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Configuration
|
||||||
|
|
||||||
|
Create a `.env` file with your configuration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
GEOGUESSR_NCFA_COOKIE=your_cookie_here
|
||||||
|
MONITORING_ENABLED=true
|
||||||
|
MONITORING_INTERVAL_HOURS=24
|
||||||
|
LOG_LEVEL=DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
### Reporting Bugs
|
||||||
|
|
||||||
|
Before creating a bug report:
|
||||||
|
1. **Check existing issues** to avoid duplicates
|
||||||
|
2. **Use the latest version** to verify the bug still exists
|
||||||
|
3. **Collect information**: version, OS, steps to reproduce
|
||||||
|
|
||||||
|
Create a detailed bug report including:
|
||||||
|
- Clear, descriptive title
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected vs. actual behavior
|
||||||
|
- Screenshots or logs (if applicable)
|
||||||
|
- Environment details
|
||||||
|
|
||||||
|
### Suggesting Enhancements
|
||||||
|
|
||||||
|
Enhancement suggestions are welcome! Include:
|
||||||
|
- Clear description of the proposed feature
|
||||||
|
- Rationale and use cases
|
||||||
|
- Examples of how it would work
|
||||||
|
- Any potential drawbacks or alternatives
|
||||||
|
|
||||||
|
### Contributing Code
|
||||||
|
|
||||||
|
1. **Find or create an issue** for the change you want to make
|
||||||
|
2. **Create a branch** from main:
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
# or
|
||||||
|
git checkout -b fix/issue-number-description
|
||||||
|
```
|
||||||
|
3. **Make your changes** following our coding standards
|
||||||
|
4. **Write tests** for your changes
|
||||||
|
5. **Run tests** to ensure nothing breaks
|
||||||
|
6. **Commit your changes** with clear messages
|
||||||
|
7. **Push to your fork** and create a pull request
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### Python Style
|
||||||
|
|
||||||
|
We follow PEP 8 with some modifications:
|
||||||
|
|
||||||
|
- **Line length**: 100 characters (Black formatter)
|
||||||
|
- **Formatting**: Use Black for automatic formatting
|
||||||
|
- **Linting**: Use Ruff for code quality checks
|
||||||
|
- **Type hints**: Required for all functions (MyPy strict mode)
|
||||||
|
|
||||||
|
### Code Quality Tools
|
||||||
|
|
||||||
|
Run before committing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
black src/ tests/
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
ruff check src/ tests/
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
mypy src/
|
||||||
|
|
||||||
|
# Run all checks
|
||||||
|
black src/ tests/ && ruff check src/ tests/ && mypy src/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Keep functions focused**: One responsibility per function
|
||||||
|
2. **Use type hints**: All parameters and return values
|
||||||
|
3. **Write docstrings**: For all public functions and classes
|
||||||
|
4. **Avoid over-engineering**: Simple solutions are preferred
|
||||||
|
5. **Handle errors gracefully**: Use proper exception handling
|
||||||
|
6. **Log appropriately**: Use logging module, not print()
|
||||||
|
|
||||||
|
### Docstring Format
|
||||||
|
|
||||||
|
Use Google-style docstrings:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def function_name(param1: str, param2: int) -> bool:
|
||||||
|
"""Brief description of function.
|
||||||
|
|
||||||
|
Longer description if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
param1: Description of param1
|
||||||
|
param2: Description of param2
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Description of return value
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: When something goes wrong
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
pytest --cov=src/geoguessr_mcp tests/
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
pytest tests/unit/test_specific.py
|
||||||
|
|
||||||
|
# Run tests matching a pattern
|
||||||
|
pytest -k "test_auth"
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
pytest -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
- **Unit tests**: Test individual components in isolation
|
||||||
|
- **Integration tests**: Test component interactions
|
||||||
|
- **Mock external calls**: Use `respx` for HTTP mocking
|
||||||
|
- **Use fixtures**: Defined in `tests/conftest.py`
|
||||||
|
- **Test coverage**: Aim for 80%+ coverage on new code
|
||||||
|
|
||||||
|
Example test:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from geoguessr_mcp.services.profile import ProfileService
|
||||||
|
|
||||||
|
async def test_get_profile_success(mock_auth_session):
|
||||||
|
"""Test successful profile retrieval."""
|
||||||
|
service = ProfileService(mock_auth_session)
|
||||||
|
profile = await service.get_profile("user123")
|
||||||
|
|
||||||
|
assert profile is not None
|
||||||
|
assert profile.id == "user123"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
### Before Submitting
|
||||||
|
|
||||||
|
- [ ] Code follows style guidelines
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] New tests added for new features
|
||||||
|
- [ ] Documentation updated (if applicable)
|
||||||
|
- [ ] Commits are clear and atomic
|
||||||
|
- [ ] No merge conflicts with main branch
|
||||||
|
|
||||||
|
### PR Guidelines
|
||||||
|
|
||||||
|
1. **Title**: Clear, concise description
|
||||||
|
2. **Description**: Explain what and why, not how
|
||||||
|
3. **Link issues**: Reference related issues
|
||||||
|
4. **Screenshots**: Include for UI changes
|
||||||
|
5. **Breaking changes**: Clearly document
|
||||||
|
|
||||||
|
### PR Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Description
|
||||||
|
Brief description of changes
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] New feature
|
||||||
|
- [ ] Breaking change
|
||||||
|
- [ ] Documentation update
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
Fixes #123
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Describe testing performed
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] Tests pass
|
||||||
|
- [ ] Code follows style guide
|
||||||
|
- [ ] Documentation updated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Review Process
|
||||||
|
|
||||||
|
1. **Automated checks**: CI/CD must pass
|
||||||
|
2. **Code review**: At least one approval required
|
||||||
|
3. **Owner approval**: Code owners must approve changes to owned files
|
||||||
|
4. **Feedback**: Address all review comments
|
||||||
|
5. **Merge**: Squash and merge (typically)
|
||||||
|
|
||||||
|
## Commit Message Guidelines
|
||||||
|
|
||||||
|
Follow conventional commits format:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <subject>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Types**:
|
||||||
|
- `feat`: New feature
|
||||||
|
- `fix`: Bug fix
|
||||||
|
- `docs`: Documentation changes
|
||||||
|
- `style`: Formatting, no code change
|
||||||
|
- `refactor`: Code restructuring
|
||||||
|
- `test`: Adding or updating tests
|
||||||
|
- `chore`: Maintenance tasks
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
```
|
||||||
|
feat(monitoring): add endpoint health checks
|
||||||
|
|
||||||
|
Implement periodic health checks for all monitored endpoints.
|
||||||
|
Includes retry logic and failure notifications.
|
||||||
|
|
||||||
|
Closes #42
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(auth): handle expired cookie gracefully
|
||||||
|
|
||||||
|
Previously, expired cookies caused unhandled exceptions.
|
||||||
|
Now we catch and re-authenticate automatically.
|
||||||
|
|
||||||
|
Fixes #58
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### When to Update Documentation
|
||||||
|
|
||||||
|
- Adding new features or tools
|
||||||
|
- Changing existing functionality
|
||||||
|
- Adding configuration options
|
||||||
|
- Updating dependencies or requirements
|
||||||
|
|
||||||
|
### Documentation Locations
|
||||||
|
|
||||||
|
- **README.md**: Overview, quick start, basic usage
|
||||||
|
- **CLAUDE.md**: Developer guide for AI assistants
|
||||||
|
- **Code comments**: Complex logic explanation
|
||||||
|
- **Docstrings**: All public APIs
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing to GeoGuessr MCP Server, you agree that your contributions will be licensed under the MIT License.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- **Questions**: Open a GitHub Discussion
|
||||||
|
- **Bugs**: Create an issue
|
||||||
|
- **Security**: Email yuki.vachot@datasingularity.fr
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
Contributors will be recognized in:
|
||||||
|
- GitHub contributors list
|
||||||
|
- Release notes (for significant contributions)
|
||||||
|
- Special mentions for major features
|
||||||
|
|
||||||
|
Thank you for contributing to GeoGuessr MCP Server! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Maintainer**: Yûki VACHOT (@NyxiumYuuki)
|
||||||
|
**Last Updated**: 2025-11-29
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Yûki VACHOT
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
138
SECURITY.md
Normal file
138
SECURITY.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We actively support the following versions of the GeoGuessr MCP Server:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 0.1.x | :white_check_mark: |
|
||||||
|
| < 0.1 | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
We take the security of the GeoGuessr MCP Server seriously. If you discover a security vulnerability, please follow these steps:
|
||||||
|
|
||||||
|
### How to Report
|
||||||
|
|
||||||
|
1. **Do NOT** open a public issue for security vulnerabilities
|
||||||
|
2. Email security details to: **yuki.vachot@datasingularity.fr**
|
||||||
|
3. Include the following information:
|
||||||
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- Potential impact assessment
|
||||||
|
- Suggested fix (if available)
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
- **Acknowledgment**: You will receive a response within 48 hours acknowledging receipt of your report
|
||||||
|
- **Investigation**: We will investigate the issue and provide an initial assessment within 5 business days
|
||||||
|
- **Updates**: We will keep you informed about the progress of the fix
|
||||||
|
- **Resolution**: Once fixed, we will notify you and coordinate disclosure timing
|
||||||
|
- **Credit**: We will credit you for the discovery (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- **Never commit** your `GEOGUESSR_NCFA_COOKIE` to version control
|
||||||
|
- Use environment variables (`.env` file) for sensitive credentials
|
||||||
|
- Rotate your cookies regularly
|
||||||
|
- Use read-only API access when possible
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
- Always use HTTPS in production environments
|
||||||
|
- Keep Docker images updated with the latest security patches
|
||||||
|
- Use secrets management for production deployments
|
||||||
|
- Implement rate limiting on public-facing endpoints
|
||||||
|
- Review and restrict container permissions
|
||||||
|
|
||||||
|
### API Usage
|
||||||
|
|
||||||
|
- Monitor API usage for unusual patterns
|
||||||
|
- Implement request validation and sanitization
|
||||||
|
- Use the latest version of dependencies
|
||||||
|
- Enable monitoring and logging for security events
|
||||||
|
|
||||||
|
## Known Security Considerations
|
||||||
|
|
||||||
|
### Authentication Token Storage
|
||||||
|
|
||||||
|
The server stores authentication cookies in memory during runtime. For production use:
|
||||||
|
- Ensure proper access controls on the server
|
||||||
|
- Use encrypted storage if persisting credentials
|
||||||
|
- Implement session timeouts
|
||||||
|
|
||||||
|
### API Monitoring
|
||||||
|
|
||||||
|
The monitoring system periodically checks GeoGuessr API endpoints:
|
||||||
|
- Requests are made with appropriate rate limiting
|
||||||
|
- No sensitive data is logged
|
||||||
|
- Schema data is stored locally without sensitive information
|
||||||
|
|
||||||
|
### Docker Security
|
||||||
|
|
||||||
|
When deploying with Docker:
|
||||||
|
- Use non-root user inside containers
|
||||||
|
- Limit container capabilities
|
||||||
|
- Use read-only root filesystem where possible
|
||||||
|
- Scan images for vulnerabilities regularly
|
||||||
|
|
||||||
|
## Dependency Security
|
||||||
|
|
||||||
|
We use automated tools to monitor dependencies:
|
||||||
|
- Regular updates via Dependabot (recommended)
|
||||||
|
- Vulnerability scanning in CI/CD pipelines
|
||||||
|
- Manual security audits of critical dependencies
|
||||||
|
|
||||||
|
### Updating Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check for security vulnerabilities
|
||||||
|
pip install safety
|
||||||
|
safety check
|
||||||
|
|
||||||
|
# Update dependencies
|
||||||
|
pip install --upgrade -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Checklist for Contributors
|
||||||
|
|
||||||
|
Before submitting a pull request, ensure:
|
||||||
|
|
||||||
|
- [ ] No hardcoded credentials or secrets
|
||||||
|
- [ ] Input validation on all user-provided data
|
||||||
|
- [ ] Proper error handling without information disclosure
|
||||||
|
- [ ] No SQL injection vulnerabilities (if using databases)
|
||||||
|
- [ ] No XSS vulnerabilities in web interfaces
|
||||||
|
- [ ] Dependencies are up to date
|
||||||
|
- [ ] Security tests are passing
|
||||||
|
- [ ] Code follows secure coding practices
|
||||||
|
|
||||||
|
## Vulnerability Disclosure Policy
|
||||||
|
|
||||||
|
We follow a coordinated disclosure policy:
|
||||||
|
|
||||||
|
1. **Private disclosure**: Vulnerabilities are reported privately
|
||||||
|
2. **Investigation period**: 90 days to develop and test a fix
|
||||||
|
3. **Coordinated release**: Fix is released with security advisory
|
||||||
|
4. **Public disclosure**: Details published after fix is available
|
||||||
|
|
||||||
|
## Security Updates
|
||||||
|
|
||||||
|
Security updates are released as:
|
||||||
|
- **Critical**: Immediate patch release
|
||||||
|
- **High**: Release within 7 days
|
||||||
|
- **Medium**: Release within 30 days
|
||||||
|
- **Low**: Included in next scheduled release
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For security-related questions or concerns:
|
||||||
|
- **Email**: yuki.vachot@datasingularity.fr
|
||||||
|
- **Response Time**: Within 48 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-11-29
|
||||||
|
|
@ -8,6 +8,4 @@ with automatic API monitoring and dynamic schema adaptation.
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.2.0"
|
||||||
__author__ = "Yûki VACHOT"
|
__author__ = "Yûki VACHOT"
|
||||||
|
|
||||||
from .main import main, mcp
|
__all__ = ["__version__", "__author__"]
|
||||||
|
|
||||||
__all__ = ["mcp", "main", "__version__", "__author__"]
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"""Monitoring module for API endpoint tracking and schema detection."""
|
"""Monitoring module for API endpoint tracking and schema detection."""
|
||||||
|
|
||||||
from .endpoint.endpoint_monitor import (MONITORED_ENDPOINTS, EndpointMonitor,
|
from .endpoint.endpoint_monitor import MONITORED_ENDPOINTS, EndpointMonitor, endpoint_monitor
|
||||||
endpoint_monitor)
|
|
||||||
from .schema.endpoint_schema import EndpointSchema
|
from .schema.endpoint_schema import EndpointSchema
|
||||||
from .schema.schema_detector import SchemaDetector, SchemaField
|
from .schema.schema_detector import SchemaDetector, SchemaField
|
||||||
from .schema.schema_registry import SchemaRegistry, schema_registry
|
from .schema.schema_registry import SchemaRegistry, schema_registry
|
||||||
|
|
|
||||||
|
|
@ -74,9 +74,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
|
||||||
"success": True,
|
"success": True,
|
||||||
"total_entries": len(entries),
|
"total_entries": len(entries),
|
||||||
"entry_types": list(categorized.keys()),
|
"entry_types": list(categorized.keys()),
|
||||||
"entries_by_type": {
|
"entries_by_type": {t: len(e) for t, e in categorized.items()},
|
||||||
t: len(e) for t, e in categorized.items()
|
|
||||||
},
|
|
||||||
"recent_entries": entries[:5], # First 5 for context
|
"recent_entries": entries[:5], # First 5 for context
|
||||||
"available_fields": response.available_fields,
|
"available_fields": response.available_fields,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ def register_monitoring_tools(mcp: FastMCP):
|
||||||
session_token = get_current_session_token()
|
session_token = get_current_session_token()
|
||||||
if session_token:
|
if session_token:
|
||||||
from ..auth.session import SessionManager
|
from ..auth.session import SessionManager
|
||||||
|
|
||||||
session_manager = SessionManager()
|
session_manager = SessionManager()
|
||||||
session = await session_manager.get_session(session_token)
|
session = await session_manager.get_session(session_token)
|
||||||
if session:
|
if session:
|
||||||
|
|
@ -136,14 +137,16 @@ def register_monitoring_tools(mcp: FastMCP):
|
||||||
previous = history[-1] if history else None
|
previous = history[-1] if history else None
|
||||||
|
|
||||||
if current and previous:
|
if current and previous:
|
||||||
changes.append({
|
changes.append(
|
||||||
"endpoint": endpoint,
|
{
|
||||||
"current_hash": current.schema_hash,
|
"endpoint": endpoint,
|
||||||
"previous_hash": previous.schema_hash,
|
"current_hash": current.schema_hash,
|
||||||
"current_fields": len(current.fields),
|
"previous_hash": previous.schema_hash,
|
||||||
"previous_fields": len(previous.fields),
|
"current_fields": len(current.fields),
|
||||||
"changed_at": current.last_updated.isoformat(),
|
"previous_fields": len(previous.fields),
|
||||||
})
|
"changed_at": current.last_updated.isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_changes_tracked": len(changes),
|
"total_changes_tracked": len(changes),
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@ def register_profile_tools(mcp: FastMCP, profile_service: ProfileService):
|
||||||
"total": len(achievements),
|
"total": len(achievements),
|
||||||
"unlocked": len(unlocked),
|
"unlocked": len(unlocked),
|
||||||
"locked": len(locked),
|
"locked": len(locked),
|
||||||
"completion_rate": f"{len(unlocked) / len(achievements) * 100:.1f}%" if achievements else "0%",
|
"completion_rate": (
|
||||||
|
f"{len(unlocked) / len(achievements) * 100:.1f}%" if achievements else "0%"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"unlocked_achievements": [
|
"unlocked_achievements": [
|
||||||
{"name": a.name, "description": a.description, "unlocked_at": a.unlocked_at}
|
{"name": a.name, "description": a.description, "unlocked_at": a.unlocked_at}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Shared test fixtures."""
|
"""Shared test fixtures."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -16,7 +17,7 @@ from geoguessr_mcp.services import AnalysisService, GameService, ProfileService
|
||||||
def mock_env(request, monkeypatch):
|
def mock_env(request, monkeypatch):
|
||||||
"""Set up environment variables for testing."""
|
"""Set up environment variables for testing."""
|
||||||
# Skip this fixture if the test has the 'real_env' marker
|
# Skip this fixture if the test has the 'real_env' marker
|
||||||
if 'real_env' in request.keywords:
|
if "real_env" in request.keywords:
|
||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ def mock_env(request, monkeypatch):
|
||||||
|
|
||||||
# Clear schema registry to avoid interference from registered schemas
|
# Clear schema registry to avoid interference from registered schemas
|
||||||
from geoguessr_mcp.monitoring.schema.schema_registry import schema_registry
|
from geoguessr_mcp.monitoring.schema.schema_registry import schema_registry
|
||||||
|
|
||||||
# Store original schemas
|
# Store original schemas
|
||||||
original_schemas = schema_registry.schemas.copy()
|
original_schemas = schema_registry.schemas.copy()
|
||||||
# Clear all schemas for testing
|
# Clear all schemas for testing
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,7 @@ class TestGeoGuessrClientIntegration:
|
||||||
"""Test real API call to profile endpoint."""
|
"""Test real API call to profile endpoint."""
|
||||||
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||||
|
|
||||||
|
|
@ -317,6 +318,7 @@ class TestGeoGuessrClientIntegration:
|
||||||
"""Test real API call to stats' endpoint."""
|
"""Test real API call to stats' endpoint."""
|
||||||
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,9 @@ class TestAnalysisService:
|
||||||
games = []
|
games = []
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
base_score = 15000 + (i * 2000) # Increasing scores
|
base_score = 15000 + (i * 2000) # Increasing scores
|
||||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)]
|
rounds = [
|
||||||
|
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)
|
||||||
|
]
|
||||||
game = Game(
|
game = Game(
|
||||||
token=f"game-{i}",
|
token=f"game-{i}",
|
||||||
map_name="World",
|
map_name="World",
|
||||||
|
|
@ -124,7 +126,9 @@ class TestAnalysisService:
|
||||||
games = []
|
games = []
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
base_score = 25000 - (i * 2000) # Decreasing scores
|
base_score = 25000 - (i * 2000) # Decreasing scores
|
||||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)]
|
rounds = [
|
||||||
|
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)
|
||||||
|
]
|
||||||
game = Game(
|
game = Game(
|
||||||
token=f"game-{i}",
|
token=f"game-{i}",
|
||||||
map_name="World",
|
map_name="World",
|
||||||
|
|
@ -182,17 +186,11 @@ class TestAnalysisService:
|
||||||
assert all(area["score"] >= 4500 for area in result.strong_areas)
|
assert all(area["score"] >= 4500 for area in result.strong_areas)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_analyze_recent_games(
|
async def test_analyze_recent_games(self, analysis_service, mock_game_service, sample_games):
|
||||||
self, analysis_service, mock_game_service, sample_games
|
|
||||||
):
|
|
||||||
"""Test analyze_recent_games method."""
|
"""Test analyze_recent_games method."""
|
||||||
mock_game_service.get_recent_games.return_value = sample_games
|
mock_game_service.get_recent_games.return_value = sample_games
|
||||||
|
|
||||||
with patch.object(
|
with patch.object(analysis_service, "analyze_games", wraps=AnalysisService.analyze_games):
|
||||||
analysis_service,
|
|
||||||
'analyze_games',
|
|
||||||
wraps=AnalysisService.analyze_games
|
|
||||||
):
|
|
||||||
result = await analysis_service.analyze_recent_games(count=5)
|
result = await analysis_service.analyze_recent_games(count=5)
|
||||||
|
|
||||||
assert "analysis" in result
|
assert "analysis" in result
|
||||||
|
|
@ -207,17 +205,20 @@ class TestAnalysisService:
|
||||||
"""Test analyze_recent_games with session token."""
|
"""Test analyze_recent_games with session token."""
|
||||||
mock_game_service.get_recent_games.return_value = sample_games
|
mock_game_service.get_recent_games.return_value = sample_games
|
||||||
|
|
||||||
await analysis_service.analyze_recent_games(
|
await analysis_service.analyze_recent_games(count=10, session_token="test_token")
|
||||||
count=10,
|
|
||||||
session_token="test_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_game_service.get_recent_games.assert_called_once_with(10, "test_token")
|
mock_game_service.get_recent_games.assert_called_once_with(10, "test_token")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_performance_summary(
|
async def test_get_performance_summary(
|
||||||
self, analysis_service, mock_game_service, mock_profile_service,
|
self,
|
||||||
mock_client, sample_games, mock_season_stats_data, mock_dynamic_response
|
analysis_service,
|
||||||
|
mock_game_service,
|
||||||
|
mock_profile_service,
|
||||||
|
mock_client,
|
||||||
|
sample_games,
|
||||||
|
mock_season_stats_data,
|
||||||
|
mock_dynamic_response,
|
||||||
):
|
):
|
||||||
"""Test comprehensive performance summary."""
|
"""Test comprehensive performance summary."""
|
||||||
mock_profile_service.get_comprehensive_profile.return_value = {
|
mock_profile_service.get_comprehensive_profile.return_value = {
|
||||||
|
|
@ -227,6 +228,7 @@ class TestAnalysisService:
|
||||||
|
|
||||||
mock_season_response = mock_dynamic_response(mock_season_stats_data)
|
mock_season_response = mock_dynamic_response(mock_season_stats_data)
|
||||||
from geoguessr_mcp.models import SeasonStats
|
from geoguessr_mcp.models import SeasonStats
|
||||||
|
|
||||||
mock_season_stats = SeasonStats.from_api_response(mock_season_stats_data)
|
mock_season_stats = SeasonStats.from_api_response(mock_season_stats_data)
|
||||||
mock_game_service.get_season_stats.return_value = (mock_season_stats, mock_season_response)
|
mock_game_service.get_season_stats.return_value = (mock_season_stats, mock_season_response)
|
||||||
mock_game_service.get_recent_games.return_value = sample_games[:3]
|
mock_game_service.get_recent_games.return_value = sample_games[:3]
|
||||||
|
|
@ -267,7 +269,14 @@ class TestAnalysisService:
|
||||||
for i in range(5)
|
for i in range(5)
|
||||||
]
|
]
|
||||||
games = [
|
games = [
|
||||||
Game(token="g1", map_name="World", mode="standard", total_score=15000, rounds=rounds, finished=True)
|
Game(
|
||||||
|
token="g1",
|
||||||
|
map_name="World",
|
||||||
|
mode="standard",
|
||||||
|
total_score=15000,
|
||||||
|
rounds=rounds,
|
||||||
|
finished=True,
|
||||||
|
)
|
||||||
for _ in range(5)
|
for _ in range(5)
|
||||||
]
|
]
|
||||||
mock_game_service.get_recent_games.return_value = games
|
mock_game_service.get_recent_games.return_value = games
|
||||||
|
|
@ -289,7 +298,14 @@ class TestAnalysisService:
|
||||||
for i in range(5)
|
for i in range(5)
|
||||||
]
|
]
|
||||||
games = [
|
games = [
|
||||||
Game(token="g1", map_name="World", mode="standard", total_score=17500, rounds=rounds, finished=True)
|
Game(
|
||||||
|
token="g1",
|
||||||
|
map_name="World",
|
||||||
|
mode="standard",
|
||||||
|
total_score=17500,
|
||||||
|
rounds=rounds,
|
||||||
|
finished=True,
|
||||||
|
)
|
||||||
for _ in range(5)
|
for _ in range(5)
|
||||||
]
|
]
|
||||||
mock_game_service.get_recent_games.return_value = games
|
mock_game_service.get_recent_games.return_value = games
|
||||||
|
|
@ -308,7 +324,9 @@ class TestAnalysisService:
|
||||||
games = []
|
games = []
|
||||||
for i in range(6):
|
for i in range(6):
|
||||||
base_score = 25000 - (i * 3000)
|
base_score = 25000 - (i * 3000)
|
||||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=45)]
|
rounds = [
|
||||||
|
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=45)
|
||||||
|
]
|
||||||
game = Game(
|
game = Game(
|
||||||
token=f"game-{i}",
|
token=f"game-{i}",
|
||||||
map_name="World",
|
map_name="World",
|
||||||
|
|
|
||||||
|
|
@ -40,32 +40,23 @@ class TestGameService:
|
||||||
"""Test game details with explicit session token."""
|
"""Test game details with explicit session token."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(mock_game_data)
|
mock_client.get.return_value = mock_dynamic_response(mock_game_data)
|
||||||
|
|
||||||
game, response = await game_service.get_game_details(
|
game, response = await game_service.get_game_details("ABC123", session_token="test_token")
|
||||||
"ABC123",
|
|
||||||
session_token="test_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
call_args = mock_client.get.call_args
|
call_args = mock_client.get.call_args
|
||||||
assert call_args[0][1] == "test_token"
|
assert call_args[0][1] == "test_token"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_game_details_failure(
|
async def test_get_game_details_failure(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test game details retrieval failure."""
|
"""Test game details retrieval failure."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response(
|
||||||
{"error": "Game not found"},
|
{"error": "Game not found"}, success=False, status_code=404
|
||||||
success=False,
|
|
||||||
status_code=404
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Failed to get game details"):
|
with pytest.raises(ValueError, match="Failed to get game details"):
|
||||||
await game_service.get_game_details("INVALID")
|
await game_service.get_game_details("INVALID")
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_unfinished_games(
|
async def test_get_unfinished_games(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test unfinished games retrieval."""
|
"""Test unfinished games retrieval."""
|
||||||
unfinished_data = [
|
unfinished_data = [
|
||||||
{"token": "game-1", "map": {"name": "World"}},
|
{"token": "game-1", "map": {"name": "World"}},
|
||||||
|
|
@ -79,9 +70,7 @@ class TestGameService:
|
||||||
assert len(response.data) == 2
|
assert len(response.data) == 2
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_streak_game(
|
async def test_get_streak_game(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test streak game retrieval."""
|
"""Test streak game retrieval."""
|
||||||
streak_data = {
|
streak_data = {
|
||||||
"token": "streak-123",
|
"token": "streak-123",
|
||||||
|
|
@ -122,7 +111,12 @@ class TestGameService:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_recent_games_success(
|
async def test_get_recent_games_success(
|
||||||
self, game_service, mock_client, mock_activity_feed_data, mock_game_data, mock_dynamic_response
|
self,
|
||||||
|
game_service,
|
||||||
|
mock_client,
|
||||||
|
mock_activity_feed_data,
|
||||||
|
mock_game_data,
|
||||||
|
mock_dynamic_response,
|
||||||
):
|
):
|
||||||
"""Test recent games retrieval."""
|
"""Test recent games retrieval."""
|
||||||
# First call returns activity feed, subsequent calls return game details
|
# First call returns activity feed, subsequent calls return game details
|
||||||
|
|
@ -153,10 +147,7 @@ class TestGameService:
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
self, game_service, mock_client, mock_dynamic_response
|
||||||
):
|
):
|
||||||
"""Test recent games when feed fails."""
|
"""Test recent games when feed fails."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response({"error": "Failed"}, success=False)
|
||||||
{"error": "Failed"},
|
|
||||||
success=False
|
|
||||||
)
|
|
||||||
|
|
||||||
games = await game_service.get_recent_games(count=5)
|
games = await game_service.get_recent_games(count=5)
|
||||||
|
|
||||||
|
|
@ -164,7 +155,12 @@ class TestGameService:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_recent_games_skips_failed_game_fetch(
|
async def test_get_recent_games_skips_failed_game_fetch(
|
||||||
self, game_service, mock_client, mock_activity_feed_data, mock_game_data, mock_dynamic_response
|
self,
|
||||||
|
game_service,
|
||||||
|
mock_client,
|
||||||
|
mock_activity_feed_data,
|
||||||
|
mock_game_data,
|
||||||
|
mock_dynamic_response,
|
||||||
):
|
):
|
||||||
"""Test that failed individual game fetches are skipped."""
|
"""Test that failed individual game fetches are skipped."""
|
||||||
mock_client.get.side_effect = [
|
mock_client.get.side_effect = [
|
||||||
|
|
@ -193,14 +189,10 @@ class TestGameService:
|
||||||
assert stats.division == "Gold"
|
assert stats.division == "Gold"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_season_stats_failure(
|
async def test_get_season_stats_failure(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test season stats failure."""
|
"""Test season stats failure."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response(
|
||||||
{"error": "No active season"},
|
{"error": "No active season"}, success=False, status_code=404
|
||||||
success=False,
|
|
||||||
status_code=404
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Failed to get season stats"):
|
with pytest.raises(ValueError, match="Failed to get season stats"):
|
||||||
|
|
@ -246,18 +238,14 @@ class TestGameService:
|
||||||
):
|
):
|
||||||
"""Test daily challenge failure."""
|
"""Test daily challenge failure."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response(
|
||||||
{"error": "Challenge not found"},
|
{"error": "Challenge not found"}, success=False, status_code=404
|
||||||
success=False,
|
|
||||||
status_code=404
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Failed to get daily challenge"):
|
with pytest.raises(ValueError, match="Failed to get daily challenge"):
|
||||||
await game_service.get_daily_challenge()
|
await game_service.get_daily_challenge()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_battle_royale(
|
async def test_get_battle_royale(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test battle royale game retrieval."""
|
"""Test battle royale game retrieval."""
|
||||||
br_data = {
|
br_data = {
|
||||||
"gameId": "br-123",
|
"gameId": "br-123",
|
||||||
|
|
@ -272,9 +260,7 @@ class TestGameService:
|
||||||
assert response.data["players"] == 10
|
assert response.data["players"] == 10
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_duel(
|
async def test_get_duel(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test duel game retrieval."""
|
"""Test duel game retrieval."""
|
||||||
duel_data = {
|
duel_data = {
|
||||||
"duelId": "duel-456",
|
"duelId": "duel-456",
|
||||||
|
|
@ -289,9 +275,7 @@ class TestGameService:
|
||||||
assert response.data["player1"]["score"] == 5000
|
assert response.data["player1"]["score"] == 5000
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_tournaments(
|
async def test_get_tournaments(self, game_service, mock_client, mock_dynamic_response):
|
||||||
self, game_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test tournaments retrieval."""
|
"""Test tournaments retrieval."""
|
||||||
tournaments_data = [
|
tournaments_data = [
|
||||||
{"id": "t1", "name": "Weekly Tournament", "status": "active"},
|
{"id": "t1", "name": "Weekly Tournament", "status": "active"},
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,10 @@ class TestProfileService:
|
||||||
assert call_args[0][1] == "test_token"
|
assert call_args[0][1] == "test_token"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_profile_failure(
|
async def test_get_profile_failure(self, profile_service, mock_client, mock_dynamic_response):
|
||||||
self, profile_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test profile retrieval failure."""
|
"""Test profile retrieval failure."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response(
|
||||||
{"error": "Unauthorized"},
|
{"error": "Unauthorized"}, success=False, status_code=401
|
||||||
success=False,
|
|
||||||
status_code=401
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Failed to get profile"):
|
with pytest.raises(ValueError, match="Failed to get profile"):
|
||||||
|
|
@ -75,23 +71,17 @@ class TestProfileService:
|
||||||
assert stats.win_rate == 0.65
|
assert stats.win_rate == 0.65
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_stats_failure(
|
async def test_get_stats_failure(self, profile_service, mock_client, mock_dynamic_response):
|
||||||
self, profile_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test stats retrieval failure."""
|
"""Test stats retrieval failure."""
|
||||||
mock_client.get.return_value = mock_dynamic_response(
|
mock_client.get.return_value = mock_dynamic_response(
|
||||||
{"error": "Server error"},
|
{"error": "Server error"}, success=False, status_code=500
|
||||||
success=False,
|
|
||||||
status_code=500
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="Failed to get stats"):
|
with pytest.raises(ValueError, match="Failed to get stats"):
|
||||||
await profile_service.get_stats()
|
await profile_service.get_stats()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_extended_stats(
|
async def test_get_extended_stats(self, profile_service, mock_client, mock_dynamic_response):
|
||||||
self, profile_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test extended stats retrieval."""
|
"""Test extended stats retrieval."""
|
||||||
extended_data = {
|
extended_data = {
|
||||||
"totalDistance": 1500000,
|
"totalDistance": 1500000,
|
||||||
|
|
@ -154,9 +144,7 @@ class TestProfileService:
|
||||||
assert achievements[0].name == "Winner"
|
assert achievements[0].name == "Winner"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_public_profile(
|
async def test_get_public_profile(self, profile_service, mock_client, mock_dynamic_response):
|
||||||
self, profile_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test public profile retrieval."""
|
"""Test public profile retrieval."""
|
||||||
public_profile_data = {
|
public_profile_data = {
|
||||||
"id": "other-user-123",
|
"id": "other-user-123",
|
||||||
|
|
@ -172,9 +160,7 @@ class TestProfileService:
|
||||||
assert profile.nick == "OtherPlayer"
|
assert profile.nick == "OtherPlayer"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_user_maps(
|
async def test_get_user_maps(self, profile_service, mock_client, mock_dynamic_response):
|
||||||
self, profile_service, mock_client, mock_dynamic_response
|
|
||||||
):
|
|
||||||
"""Test user maps retrieval."""
|
"""Test user maps retrieval."""
|
||||||
maps_data = [
|
maps_data = [
|
||||||
{"id": "map-1", "name": "My Custom Map"},
|
{"id": "map-1", "name": "My Custom Map"},
|
||||||
|
|
@ -189,7 +175,12 @@ class TestProfileService:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_comprehensive_profile_success(
|
async def test_get_comprehensive_profile_success(
|
||||||
self, profile_service, mock_client, mock_profile_data, mock_stats_data, mock_dynamic_response
|
self,
|
||||||
|
profile_service,
|
||||||
|
mock_client,
|
||||||
|
mock_profile_data,
|
||||||
|
mock_stats_data,
|
||||||
|
mock_dynamic_response,
|
||||||
):
|
):
|
||||||
"""Test comprehensive profile aggregation."""
|
"""Test comprehensive profile aggregation."""
|
||||||
# Setup mock responses for each call
|
# Setup mock responses for each call
|
||||||
|
|
@ -197,9 +188,11 @@ class TestProfileService:
|
||||||
mock_dynamic_response(mock_profile_data), # profile
|
mock_dynamic_response(mock_profile_data), # profile
|
||||||
mock_dynamic_response(mock_stats_data), # stats
|
mock_dynamic_response(mock_stats_data), # stats
|
||||||
mock_dynamic_response({"totalDistance": 1000}), # extended stats
|
mock_dynamic_response({"totalDistance": 1000}), # extended stats
|
||||||
mock_dynamic_response([ # achievements
|
mock_dynamic_response(
|
||||||
{"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"},
|
[ # achievements
|
||||||
]),
|
{"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"},
|
||||||
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
result = await profile_service.get_comprehensive_profile()
|
result = await profile_service.get_comprehensive_profile()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 1 - v4/getActivities
|
name: 1 - v4/getActivities
|
||||||
type: http
|
type: http
|
||||||
seq: 3
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 10 - v3/getSocialBadgesUnclaimed
|
name: 10 - v3/getSocialBadgesUnclaimed
|
||||||
type: http
|
type: http
|
||||||
seq: 10
|
seq: 8
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 11 - v3/getSocialEventsUnfinishedGames
|
name: 11 - v3/getSocialEventsUnfinishedGames
|
||||||
type: http
|
type: http
|
||||||
seq: 11
|
seq: 9
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 12 - v3/getProfilesMaps
|
name: 12 - v3/getProfilesMaps
|
||||||
type: http
|
type: http
|
||||||
seq: 12
|
seq: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 13 - v3/getMapLikes
|
name: 13 - v3/getMapLikes
|
||||||
type: http
|
type: http
|
||||||
seq: 13
|
seq: 11
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 14 - v4/getStatsMe
|
name: 14 - v4/getStatsMe
|
||||||
type: http
|
type: http
|
||||||
seq: 14
|
seq: 12
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 15 - v3/getProfilesStats
|
name: 15 - v3/getProfilesStats
|
||||||
type: http
|
type: http
|
||||||
seq: 15
|
seq: 13
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 16 - v3/getSubscriptions
|
name: 16 - v3/getSubscriptions
|
||||||
type: http
|
type: http
|
||||||
seq: 16
|
seq: 14
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 17 - v3/getExplorer
|
name: 17 - v3/getExplorer
|
||||||
type: http
|
type: http
|
||||||
seq: 17
|
seq: 15
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 18 - v3/postAccountsSignin
|
name: 18 - v3/postAccountsSignin
|
||||||
type: http
|
type: http
|
||||||
seq: 18
|
seq: 16
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 19 - v3/postAccountsSignout
|
name: 19 - v3/postAccountsSignout
|
||||||
type: http
|
type: http
|
||||||
seq: 19
|
seq: 17
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 2 - v3/getProfiles
|
name: 2 - v3/getProfiles
|
||||||
type: http
|
type: http
|
||||||
seq: 4
|
seq: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 20 - v4/getTrophiesUser
|
name: 20 - v4/getTrophiesUser
|
||||||
type: http
|
type: http
|
||||||
seq: 20
|
seq: 18
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 21 - v3/getGamesGame
|
name: 21 - v3/getGamesGame
|
||||||
type: http
|
type: http
|
||||||
seq: 21
|
seq: 19
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 22 - v3/getSocialMapsBrowsePopularRandom
|
name: 22 - v3/getSocialMapsBrowsePopularRandom
|
||||||
type: http
|
type: http
|
||||||
seq: 22
|
seq: 20
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 23_1 - v3/getChallengesDailyChallengesToday
|
name: 23_1 - v3/getChallengesDailyChallengesToday
|
||||||
type: http
|
type: http
|
||||||
seq: 23
|
seq: 21
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 23_2 - v3/getChallengesDailyChallengesPrevious
|
name: 23_2 - v3/getChallengesDailyChallengesPrevious
|
||||||
type: http
|
type: http
|
||||||
seq: 24
|
seq: 22
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 24 - v3/getChallengesChallenge
|
name: 24 - v3/getChallengesChallenge
|
||||||
type: http
|
type: http
|
||||||
seq: 25
|
seq: 23
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 28 - v3/getExperiments
|
name: 28 - v3/getExperiments
|
||||||
type: http
|
type: http
|
||||||
seq: 26
|
seq: 24
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 3 - v4/getNotifications
|
name: 3 - v4/getNotifications
|
||||||
type: http
|
type: http
|
||||||
seq: 5
|
seq: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 30 - maps/getMap
|
name: 30 - maps/getMap
|
||||||
type: http
|
type: http
|
||||||
seq: 27
|
seq: 25
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 31 - game-server/getBattleRoyaleGame
|
name: 31 - game-server/getBattleRoyaleGame
|
||||||
type: http
|
type: http
|
||||||
seq: 28
|
seq: 26
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 32 - game-server/getLobbyGame
|
name: 32 - game-server/getLobbyGame
|
||||||
type: http
|
type: http
|
||||||
seq: 29
|
seq: 27
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 35 - game-server/getTournaments
|
name: 35 - game-server/getTournaments
|
||||||
type: http
|
type: http
|
||||||
seq: 30
|
seq: 28
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 4 - v3/getFriends
|
name: 4 - v3/getFriends
|
||||||
type: http
|
type: http
|
||||||
seq: 6
|
seq: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 5 - v3/getFriendsSummary
|
name: 5 - v3/getFriendsSummary
|
||||||
type: http
|
type: http
|
||||||
seq: 7
|
seq: 5
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 6 - v3/getFriendships
|
name: 6 - v3/getFriendships
|
||||||
type: http
|
type: http
|
||||||
seq: 8
|
seq: 6
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: 9 - v3/getPersonalizedMap
|
name: 9 - v3/getPersonalizedMap
|
||||||
type: http
|
type: http
|
||||||
seq: 9
|
seq: 7
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: README (Docs)
|
name: README (Docs)
|
||||||
type: http
|
type: http
|
||||||
seq: 2
|
seq: 29
|
||||||
}
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
meta {
|
meta {
|
||||||
name: deprecated
|
name: deprecated
|
||||||
seq: 31
|
seq: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
auth {
|
auth {
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
meta {
|
||||||
|
name: Geoguessr API
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
meta {
|
||||||
|
name: MCP Server Test
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
mode: inherit
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
meta {
|
||||||
|
name: getProfile
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{MCP_SERVER_URL}}/mcp
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept: application/json, text/event-stream
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "get_my_profile",
|
||||||
|
"params": {},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
vars {
|
vars {
|
||||||
GEOGUESSR_API_URL: https://www.geoguessr.com/api
|
GEOGUESSR_API_URL: https://www.geoguessr.com/api
|
||||||
GAME_SERVER_URL: https://game-server.geoguessr.com/api
|
GAME_SERVER_URL: https://game-server.geoguessr.com/api
|
||||||
|
MCP_SERVER_URL: http://localhost:8000
|
||||||
}
|
}
|
||||||
vars:secret [
|
vars:secret [
|
||||||
GEOGUESSR_NCFA_COOKIE
|
GEOGUESSR_NCFA_COOKIE
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue