Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d85b72bc08 | |||
| 0d5b13c6d2 | |||
| 2521e9bc9d | |||
| 282c82b2c9 | |||
| f413dc7576 | |||
| cffec87011 | |||
| bdb09ceb96 | |||
| 0780f0b7d9 | |||
| 2cd5cbca12 | |||
| 2b8e8598fe | |||
| a30837a130 | |||
| 0a43bb8539 | |||
| c84924a5a1 | |||
| 4d7844e90a | |||
| e08f37b2cc | |||
| e8495800e6 | |||
| cbcb6d1dcb | |||
| 3d40bfdf76 | |||
| a3cfb4f40d | |||
| 27ddd22a2d | |||
| 8440a3daec | |||
| 39ab6cf745 | |||
| 0b71c51dd5 | |||
| 6094dd4cd1 | |||
| ee74991175 | |||
| f7596976da | |||
| b0f388161a | |||
| df1ed31ef9 | |||
| 0b8c50b93f | |||
| 6061be119c | |||
| 69fc2054d8 | |||
| cbc2fcd7c7 | |||
| c6339ad084 | |||
| c736e3ebf9 | |||
| 681e059870 | |||
| fcc8f924d3 | |||
| c2de0aa7c0 | |||
| 9c3ab2e65d | |||
| 0f0719f82f | |||
| cdf9d87adc | |||
| d9f61437a4 | |||
| db3f35b8f7 | |||
| 9399c06ec1 | |||
| 81027d5914 | |||
| 2b51266392 | |||
| 400e9d8806 |
@@ -0,0 +1,59 @@
|
||||
name: PHP Integration Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
mongo:
|
||||
image: mongo:8
|
||||
options: >-
|
||||
--health-cmd "mongosh --quiet --eval \"db.adminCommand('ping')\""
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
|
||||
steps:
|
||||
- name: Retrieve Server Install Action
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: Nodarx/action-server-install
|
||||
ref: main
|
||||
path: action-server-install
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install server
|
||||
uses: ./action-server-install
|
||||
with:
|
||||
install-php: 'true'
|
||||
php-version: '8.5'
|
||||
server-path: './server'
|
||||
database-uri: 'mongodb://mongo:27017/?tls=false'
|
||||
database-name: 'ktrix_ci'
|
||||
app-environment: 'test'
|
||||
|
||||
- name: Checkout module under test
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
path: server/modules/mail_manager
|
||||
github-server-url: https://git.ktrix.dev
|
||||
|
||||
- name: Install module dependencies
|
||||
run: composer install --prefer-dist --no-progress
|
||||
working-directory: server/modules/mail_manager
|
||||
|
||||
- name: Install and enable module
|
||||
working-directory: server
|
||||
run: |
|
||||
php bin/console module:install mail_manager
|
||||
php bin/console module:enable mail_manager
|
||||
|
||||
- name: Run integration tests
|
||||
working-directory: server/modules/mail_manager
|
||||
run: composer test:integration
|
||||
@@ -25,11 +25,17 @@ jobs:
|
||||
tools: composer:v2
|
||||
|
||||
- name: Install Renovate
|
||||
run: npm install -g renovate
|
||||
run: |
|
||||
npm install --global --no-audit --fund=false \
|
||||
--prefix "${{ runner.temp }}/renovate-npm" \
|
||||
--cache "${{ runner.temp }}/renovate-npm-cache" \
|
||||
renovate
|
||||
"${{ runner.temp }}/renovate-npm/bin/renovate" --version
|
||||
|
||||
- name: Run Renovate
|
||||
env:
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
||||
run: renovate ${{ gitea.repository }}
|
||||
run: |
|
||||
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
|
||||
|
||||
+1
-5
@@ -14,11 +14,7 @@ node_modules/
|
||||
# Backend development
|
||||
/lib/vendor/
|
||||
coverage/
|
||||
phpunit.xml.cache
|
||||
.phpunit.cache
|
||||
.phpunit.result.cache
|
||||
.php-cs-fixer.cache
|
||||
.phpstan.cache
|
||||
*.cache
|
||||
.phpactor/
|
||||
|
||||
# Editors
|
||||
|
||||
+6
-5
@@ -10,16 +10,16 @@
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"platform": {
|
||||
"php": "8.2"
|
||||
"php": "8.3"
|
||||
},
|
||||
"autoloader-suffix": "MailManager",
|
||||
"vendor-dir": "lib/vendor"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2 <=8.5"
|
||||
"php": ">=8.3 <=8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -36,7 +36,8 @@
|
||||
],
|
||||
"post-update-cmd": [
|
||||
],
|
||||
"test:unit": "phpunit --configuration tests/php/phpunit.unit.xml --colors=always --testdox",
|
||||
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.unit.xml --coverage-html .phpunit.coverage --coverage-text"
|
||||
"test:unit": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Unit Tests\" --colors=always --testdox",
|
||||
"test:integration": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Integration Tests\" --colors=always --testdox",
|
||||
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.xml --testsuite \"Unit Tests\" --coverage-html .phpunit.coverage --coverage-text"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+251
-351
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,8 @@ use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Http\Response\StreamedNdJsonResponse;
|
||||
use KTXC\Http\Response\StreamedResponse;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Json\JsonSerializable;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
@@ -23,6 +23,7 @@ use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifier;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||
use KTXF\Resource\Identifier\ServiceIdentifier;
|
||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use KTXM\MailManager\Manager;
|
||||
@@ -50,8 +51,8 @@ class DefaultController extends ControllerAbstract {
|
||||
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly TenantContextInterface $tenantContext,
|
||||
private readonly IdentityContextInterface $identityContext,
|
||||
private Manager $manager,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
@@ -79,8 +80,26 @@ class DefaultController extends ControllerAbstract {
|
||||
): Response {
|
||||
|
||||
// authorize request
|
||||
$tenantId = $this->tenantIdentity->identifier();
|
||||
$userId = $this->userIdentity->identifier();
|
||||
$tenantId = $this->tenantContext->identifier();
|
||||
$userId = $this->identityContext->identifier();
|
||||
|
||||
// acting-user override: only the reserved system context is permitted,
|
||||
// gated on the system mail management permission
|
||||
if ($user !== null && $user !== $userId) {
|
||||
if ($user !== ProviderBaseInterface::USER_SYSTEM || !$this->identityContext->hasPermission('mail_manager.system')) {
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => [
|
||||
'code' => JsonResponse::HTTP_FORBIDDEN,
|
||||
'message' => 'Not permitted to act as user: ' . $user
|
||||
]
|
||||
], JsonResponse::HTTP_FORBIDDEN);
|
||||
}
|
||||
$userId = $user;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
+14
-6
@@ -236,7 +236,14 @@ class Manager {
|
||||
$serviceId = $provider->serviceCreate($tenantId, $userId, $service);
|
||||
|
||||
// Fetch and return the created service
|
||||
return $provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||
$createdService = $provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||
if ($createdService === null) {
|
||||
throw new \RuntimeException(
|
||||
"Provider '$providerId' created service '$serviceId', but it could not be fetched"
|
||||
);
|
||||
}
|
||||
|
||||
return $createdService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1264,9 +1271,13 @@ class Manager {
|
||||
}
|
||||
|
||||
public function entitySubmit(string $tenantId, string $userId, AddressInterface|string $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|array|null $message = null): EntitySubmitResult {
|
||||
$service = $this->serviceFindByAddress($tenantId, $userId, $sender);
|
||||
if ($sender instanceof AddressInterface === false) {
|
||||
$sender = new Address($sender);
|
||||
}
|
||||
|
||||
$service = $this->serviceFindByAddress($tenantId, $userId, $sender->getAddress());
|
||||
if ($service === null || $service->getEnabled() === false) {
|
||||
throw new InvalidArgumentException("Service not found for sender '{$sender}' or service is disabled");
|
||||
throw new InvalidArgumentException("Service not found for sender '{$sender->getAddress()}' or service is disabled");
|
||||
}
|
||||
if ($service instanceof ServiceEntitySubmitInterface === false) {
|
||||
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support entity submission");
|
||||
@@ -1276,9 +1287,6 @@ class Manager {
|
||||
throw new InvalidArgumentException("At least one of source or message must be provided for entity submission");
|
||||
}
|
||||
|
||||
if ($sender instanceof AddressInterface === false) {
|
||||
$sender = new Address($sender);
|
||||
}
|
||||
if ($message !== null && $message instanceof MessagePropertiesMutableInterface === false) {
|
||||
$message = $service->entityFresh()->getProperties()->jsonDeserialize($message);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
||||
'description' => 'View and access the mail manager module',
|
||||
'group' => 'Mail Management'
|
||||
],
|
||||
'mail_manager.system' => [
|
||||
'label' => 'Manage System Mail',
|
||||
'description' => 'Manage system mail accounts and routing rules (act in the reserved system user context)',
|
||||
'group' => 'Mail Management'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Generated
+1206
-493
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -18,18 +18,21 @@
|
||||
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.0",
|
||||
"pinia": "^4.0.0",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^5.0.0",
|
||||
"vue-router": "^5.2.0",
|
||||
"vuetify": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"@vue/test-utils": "^2.4.10",
|
||||
"@vue/tsconfig": "^0.9.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.0.18",
|
||||
"vue-tsc": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ const MANUAL_STEPS = {
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
user?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -242,7 +243,8 @@ async function handleDiscover() {
|
||||
discoverSecret.value || undefined,
|
||||
discoverHostname.value || undefined,
|
||||
identifier,
|
||||
(service) => { discoveredService = service }
|
||||
(service) => { discoveredService = service },
|
||||
props.user
|
||||
)
|
||||
|
||||
// Success - check if we got results for this provider
|
||||
@@ -384,7 +386,8 @@ async function testConnection() {
|
||||
selectedProvider.value.identifier,
|
||||
null,
|
||||
selectedService.value.location,
|
||||
selectedService.value.identity
|
||||
selectedService.value.identity,
|
||||
props.user
|
||||
)
|
||||
|
||||
return testResult
|
||||
@@ -410,7 +413,8 @@ async function saveAccount() {
|
||||
|
||||
await servicesStore.create(
|
||||
selectedProvider.value.identifier,
|
||||
accountData
|
||||
accountData,
|
||||
props.user
|
||||
)
|
||||
|
||||
emit('saved')
|
||||
|
||||
@@ -14,6 +14,7 @@ const props = defineProps<{
|
||||
modelValue: boolean
|
||||
serviceProvider: string
|
||||
serviceIdentifier: string | number
|
||||
user?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -103,7 +104,10 @@ async function load() {
|
||||
try {
|
||||
const [provider, service] = await Promise.all([
|
||||
providersStore.provider(props.serviceProvider) ?? providersStore.fetch(props.serviceProvider),
|
||||
servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
||||
// acting-user context always fetches fresh, bypassing the shared cache
|
||||
props.user
|
||||
? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier, props.user)
|
||||
: servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
||||
])
|
||||
|
||||
localProvider.value = provider.clone()
|
||||
@@ -161,12 +165,16 @@ async function testConnection() {
|
||||
localService.value.provider,
|
||||
null,
|
||||
localService.value.location,
|
||||
localService.value.identity
|
||||
localService.value.identity,
|
||||
props.user
|
||||
)
|
||||
} else {
|
||||
testResult = await servicesStore.test(
|
||||
localService.value.provider,
|
||||
localService.value.identifier
|
||||
localService.value.identifier,
|
||||
undefined,
|
||||
undefined,
|
||||
props.user
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,7 +207,8 @@ async function saveAccount() {
|
||||
localService.value.provider,
|
||||
localService.value.identifier as string | number,
|
||||
true, // delta update
|
||||
localService.value
|
||||
localService.value,
|
||||
props.user
|
||||
)
|
||||
|
||||
emit('saved')
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ const routes = [
|
||||
{
|
||||
name: 'mail-accounts',
|
||||
path: '/accounts',
|
||||
component: () => import('@/pages/AccountsPage.vue'),
|
||||
component: () => import('@/pages/Main.vue'),
|
||||
meta: {
|
||||
title: 'Mail Accounts',
|
||||
requiresAuth: true
|
||||
|
||||
@@ -47,8 +47,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with service object list grouped by provider and keyed by service identifier
|
||||
*/
|
||||
async list(request: ServiceListRequest = {}): Promise<Record<string, Record<string, ServiceObject>>> {
|
||||
const response = await transceivePost<ServiceListRequest, ServiceListResponse>('service.list', request);
|
||||
async list(request: ServiceListRequest = {}, user?: string): Promise<Record<string, Record<string, ServiceObject>>> {
|
||||
const response = await transceivePost<ServiceListRequest, ServiceListResponse>('service.list', request, user);
|
||||
|
||||
// Convert nested response to ServiceObject instances
|
||||
const providerList: Record<string, Record<string, ServiceObject>> = {};
|
||||
@@ -70,8 +70,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with service object
|
||||
*/
|
||||
async fetch(request: ServiceFetchRequest): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceFetchRequest, ServiceFetchResponse>('service.fetch', request);
|
||||
async fetch(request: ServiceFetchRequest, user?: string): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceFetchRequest, ServiceFetchResponse>('service.fetch', request, user);
|
||||
return createServiceObject(response);
|
||||
},
|
||||
|
||||
@@ -82,8 +82,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with service availability status
|
||||
*/
|
||||
async extant(request: ServiceExtantRequest): Promise<ServiceExtantResponse> {
|
||||
return await transceivePost<ServiceExtantRequest, ServiceExtantResponse>('service.extant', request);
|
||||
async extant(request: ServiceExtantRequest, user?: string): Promise<ServiceExtantResponse> {
|
||||
return await transceivePost<ServiceExtantRequest, ServiceExtantResponse>('service.extant', request, user);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -96,7 +96,8 @@ export const serviceService = {
|
||||
*/
|
||||
async discover(
|
||||
request: ServiceDiscoverRequest,
|
||||
onService: (service: ServiceObject) => void
|
||||
onService: (service: ServiceObject) => void,
|
||||
user?: string
|
||||
): Promise<{ total: number }> {
|
||||
return await transceiveStream<ServiceDiscoverRequest, ServiceDiscoverResponse>(
|
||||
'service.discover',
|
||||
@@ -107,11 +108,12 @@ export const serviceService = {
|
||||
provider: service.provider,
|
||||
identifier: null,
|
||||
label: null,
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
location: service.location,
|
||||
};
|
||||
onService(createServiceObject(serviceData));
|
||||
}
|
||||
},
|
||||
user
|
||||
);
|
||||
},
|
||||
|
||||
@@ -121,8 +123,8 @@ export const serviceService = {
|
||||
* @param request - Service test request
|
||||
* @returns Promise with test results
|
||||
*/
|
||||
async test(request: ServiceTestRequest): Promise<ServiceTestResponse> {
|
||||
return await transceivePost<ServiceTestRequest, ServiceTestResponse>('service.test', request);
|
||||
async test(request: ServiceTestRequest, user?: string): Promise<ServiceTestResponse> {
|
||||
return await transceivePost<ServiceTestRequest, ServiceTestResponse>('service.test', request, user);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -132,8 +134,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with created service object
|
||||
*/
|
||||
async create(request: ServiceCreateRequest): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceCreateRequest, ServiceCreateResponse>('service.create', request);
|
||||
async create(request: ServiceCreateRequest, user?: string): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceCreateRequest, ServiceCreateResponse>('service.create', request, user);
|
||||
return createServiceObject(response);
|
||||
},
|
||||
|
||||
@@ -144,8 +146,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with updated service object
|
||||
*/
|
||||
async update(request: ServiceUpdateRequest): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceUpdateRequest, ServiceUpdateResponse>('service.update', request);
|
||||
async update(request: ServiceUpdateRequest, user?: string): Promise<ServiceObject> {
|
||||
const response = await transceivePost<ServiceUpdateRequest, ServiceUpdateResponse>('service.update', request, user);
|
||||
return createServiceObject(response);
|
||||
},
|
||||
|
||||
@@ -156,8 +158,8 @@ export const serviceService = {
|
||||
*
|
||||
* @returns Promise with deletion result
|
||||
*/
|
||||
async delete(request: { provider: string; identifier: string | number }): Promise<any> {
|
||||
return await transceivePost<ServiceDeleteRequest, ServiceDeleteResponse>('service.delete', request);
|
||||
async delete(request: { provider: string; identifier: string | number }, user?: string): Promise<any> {
|
||||
return await transceivePost<ServiceDeleteRequest, ServiceDeleteResponse>('service.delete', request, user);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+36
-23
@@ -131,10 +131,10 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with service object list keyed by provider and service identifier
|
||||
*/
|
||||
async function list(targets?: ServiceIdentifier[] | CollectionIdentifier[]): Promise<Record<string, ServiceObject>> {
|
||||
async function list(targets?: ServiceIdentifier[] | CollectionIdentifier[], user?: string): Promise<Record<string, ServiceObject>> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await serviceService.list({ targets })
|
||||
const response = await serviceService.list({ targets }, user)
|
||||
|
||||
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
|
||||
const services: Record<string, ServiceObject> = {}
|
||||
@@ -145,8 +145,10 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Merge retrieved services into state
|
||||
_services.value = { ..._services.value, ...services }
|
||||
// Merge retrieved services into state (acting-user context stays out of the shared cache)
|
||||
if (!user) {
|
||||
_services.value = { ..._services.value, ...services }
|
||||
}
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(services).length, 'services')
|
||||
return services
|
||||
@@ -166,14 +168,16 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with service object
|
||||
*/
|
||||
async function fetch(provider: string, identifier: string | number): Promise<ServiceObject> {
|
||||
async function fetch(provider: string, identifier: string | number, user?: string): Promise<ServiceObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const service = await serviceService.fetch({ provider, identifier })
|
||||
const service = await serviceService.fetch({ provider, identifier }, user)
|
||||
|
||||
// Merge fetched service into state
|
||||
// Merge fetched service into state (acting-user context stays out of the shared cache)
|
||||
const key = identifierKey(service.provider, service.identifier)
|
||||
_services.value[key] = service
|
||||
if (!user) {
|
||||
_services.value[key] = service
|
||||
}
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully fetched service:', key)
|
||||
return service
|
||||
@@ -192,10 +196,10 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with service availability status
|
||||
*/
|
||||
async function extant(targets: ServiceIdentifier[]) {
|
||||
async function extant(targets: ServiceIdentifier[], user?: string) {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const response = await serviceService.extant({ targets })
|
||||
const response = await serviceService.extant({ targets }, user)
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully checked', targets?.length ?? 0, 'services')
|
||||
return response
|
||||
@@ -215,14 +219,16 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with created service object
|
||||
*/
|
||||
async function create(provider: string, data: Partial<ServiceInterface>): Promise<ServiceObject> {
|
||||
async function create(provider: string, data: Partial<ServiceInterface>, user?: string): Promise<ServiceObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
const service = await serviceService.create({ provider, data })
|
||||
const service = await serviceService.create({ provider, data }, user)
|
||||
|
||||
// Merge created service into state
|
||||
// Merge created service into state (acting-user context stays out of the shared cache)
|
||||
const key = identifierKey(service.provider, service.identifier)
|
||||
_services.value[key] = service
|
||||
if (!user) {
|
||||
_services.value[key] = service
|
||||
}
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully created service:', key)
|
||||
return service
|
||||
@@ -244,7 +250,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with updated service object
|
||||
*/
|
||||
async function update(provider: string, identifier: string | number, delta: boolean, data: ServiceObject | Partial<ServiceInterface>): Promise<ServiceObject> {
|
||||
async function update(provider: string, identifier: string | number, delta: boolean, data: ServiceObject | Partial<ServiceInterface>, user?: string): Promise<ServiceObject> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
// convert ServiceObject to JSON if needed
|
||||
@@ -255,11 +261,13 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
payload = data
|
||||
}
|
||||
|
||||
const service = await serviceService.update({ provider, identifier, delta, data: payload })
|
||||
const service = await serviceService.update({ provider, identifier, delta, data: payload }, user)
|
||||
|
||||
// Merge updated service into state
|
||||
// Merge updated service into state (acting-user context stays out of the shared cache)
|
||||
const key = identifierKey(service.provider, service.identifier)
|
||||
_services.value[key] = service
|
||||
if (!user) {
|
||||
_services.value[key] = service
|
||||
}
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully updated service:', key)
|
||||
return service
|
||||
@@ -279,14 +287,16 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
*
|
||||
* @returns Promise with deletion result
|
||||
*/
|
||||
async function remove(provider: string, identifier: string | number): Promise<any> {
|
||||
async function remove(provider: string, identifier: string | number, user?: string): Promise<any> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
await serviceService.delete({ provider, identifier })
|
||||
await serviceService.delete({ provider, identifier }, user)
|
||||
|
||||
// Remove deleted service from state
|
||||
const key = identifierKey(provider, identifier)
|
||||
delete _services.value[key]
|
||||
if (!user) {
|
||||
delete _services.value[key]
|
||||
}
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully deleted service:', key)
|
||||
} catch (error: any) {
|
||||
@@ -314,6 +324,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
location: string | undefined,
|
||||
provider: string | undefined,
|
||||
onService?: (service: ServiceObject) => void,
|
||||
user?: string,
|
||||
): Promise<{ total: number }> {
|
||||
transceiving.value = true
|
||||
|
||||
@@ -322,7 +333,8 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
{ identity, secret, location, provider },
|
||||
(service: ServiceObject) => {
|
||||
onService?.(service)
|
||||
}
|
||||
},
|
||||
user
|
||||
)
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully discovered', result.total, 'services')
|
||||
@@ -350,6 +362,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
identifier?: string | number | null,
|
||||
location?: ServiceLocation | Location | null,
|
||||
identity?: ServiceIdentity | Identity | null,
|
||||
user?: string,
|
||||
): Promise<any> {
|
||||
transceiving.value = true
|
||||
try {
|
||||
@@ -372,7 +385,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
||||
identity = identity.toJson()
|
||||
}
|
||||
|
||||
const response = await serviceService.test({ provider, identifier, location, identity })
|
||||
const response = await serviceService.test({ provider, identifier, location, identity }, user)
|
||||
|
||||
console.debug('[Mail Manager][Store] - Successfully tested service:', provider, identifier || location)
|
||||
return response
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
describe('Basic Tests', () => {
|
||||
it('should perform basic assertion', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
it('should test array operations', () => {
|
||||
const array = ['foo', 'bar', 'baz']
|
||||
|
||||
expect(array).toHaveLength(3)
|
||||
expect(array).toContain('bar')
|
||||
expect(array[0]).toBe('foo')
|
||||
})
|
||||
|
||||
it('should test string operations', () => {
|
||||
const string = 'Hello, World!'
|
||||
|
||||
expect(string).toContain('World')
|
||||
expect(string.length).toBe(13)
|
||||
})
|
||||
|
||||
it('should test object operations', () => {
|
||||
const obj = { foo: 'bar', count: 42 }
|
||||
|
||||
expect(obj).toHaveProperty('foo')
|
||||
expect(obj.foo).toBe('bar')
|
||||
expect(obj.count).toBeGreaterThan(40)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig, configDefaults } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../../src'),
|
||||
'@KTXC': path.resolve(__dirname, '../../../../core/src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('../../', import.meta.url)),
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'tests/',
|
||||
'**/*.d.ts',
|
||||
'**/*.config.*',
|
||||
'**/dist/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace KTXT\MailManager\Tests\Integration;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BaseTest extends TestCase
|
||||
{
|
||||
public function testBasicAssertion(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function testArrayOperations(): void
|
||||
{
|
||||
$array = ['foo' => 'bar'];
|
||||
|
||||
$this->assertArrayHasKey('foo', $array);
|
||||
$this->assertEquals('bar', $array['foo']);
|
||||
}
|
||||
|
||||
public function testStringOperations(): void
|
||||
{
|
||||
$string = 'Hello, World!';
|
||||
|
||||
$this->assertStringContainsString('World', $string);
|
||||
$this->assertEquals(13, strlen($string));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,16 @@
|
||||
|
||||
require dirname(__DIR__, 2).'/lib/vendor/autoload.php';
|
||||
|
||||
// When this module is checked out inside a full server (server/modules/<handle>,
|
||||
// as it is in CI and in this monorepo checkout), also load the server's own
|
||||
// core/shared autoloader so tests can reference framework (KTXC/KTXF) types.
|
||||
// Standalone module checkouts without a server alongside them skip this.
|
||||
define('SERVER_ROOT', dirname(__DIR__, 4));
|
||||
$serverAutoload = SERVER_ROOT . '/vendor/autoload.php';
|
||||
if (is_file($serverAutoload)) {
|
||||
require $serverAutoload;
|
||||
}
|
||||
|
||||
if (isset($_SERVER['APP_DEBUG']) && $_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
<testsuite name="Unit Tests">
|
||||
<directory>unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Integration Tests">
|
||||
<directory>Integration</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true"
|
||||
@@ -29,8 +32,7 @@
|
||||
restrictWarnings="true"
|
||||
>
|
||||
<include>
|
||||
<directory>../../core/lib</directory>
|
||||
<directory>../../shared/lib</directory>
|
||||
<directory>../../lib</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
Reference in New Issue
Block a user