35 Commits

Author SHA1 Message Date
Sebastian d85b72bc08 chore(deps): update dependency phpunit/phpunit to v12.5.33
Build Test / test (pull_request) Successful in 1m9s
JS Unit Tests / test (pull_request) Successful in 54s
PHP Integration Tests / Integration Tests (pull_request) Failing after 58s
PHP Unit Tests / test (pull_request) Successful in 1m28s
2026-07-29 03:02:48 +00:00
Sebastian 0d5b13c6d2 refactor: migrate to scoped execution contexts
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-27 00:52:29 -04:00
Sebastian 2521e9bc9d Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.8' (#65) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #65
2026-07-25 23:53:08 +00:00
Sebastian 282c82b2c9 Merge pull request 'chore(deps): update dependency vite to v8.1.5' (#66) from renovate/vite-8.x-lockfile into main
Reviewed-on: #66
2026-07-25 23:53:01 +00:00
Sebastian f413dc7576 chore(deps): update dependency vite to v8.1.5
Build Test / test (pull_request) Successful in 58s
JS Unit Tests / test (pull_request) Successful in 58s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m38s
PHP Unit Tests / test (pull_request) Successful in 1m35s
2026-07-25 03:03:48 +00:00
Sebastian cffec87011 chore(deps): update dependency @vitejs/plugin-vue to v6.0.8
Build Test / test (pull_request) Successful in 48s
JS Unit Tests / test (pull_request) Successful in 53s
PHP Unit Tests / test (pull_request) Successful in 1m43s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m49s
2026-07-25 03:03:45 +00:00
Sebastian bdb09ceb96 Merge pull request 'chore: implement js test suites' (#64) from chore/js-test-suites into main
Reviewed-on: #64
2026-07-24 04:46:40 +00:00
Sebastian 0780f0b7d9 fix: update vue-router to 5.2.0 for pinia 4 peer dep support
Build Test / test (pull_request) Successful in 45s
JS Unit Tests / test (pull_request) Successful in 46s
PHP Unit Tests / test (pull_request) Successful in 1m14s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m28s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-24 00:42:42 -04:00
Sebastian 2cd5cbca12 chore: implement js test suites
JS Unit Tests / test (pull_request) Failing after 39s
Build Test / test (pull_request) Failing after 42s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m13s
PHP Unit Tests / test (pull_request) Successful in 1m42s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-24 00:32:42 -04:00
Sebastian 2b8e8598fe Merge pull request 'fix(deps): update dependency pinia to v4' (#62) from renovate/pinia-4.x into main
Reviewed-on: #62
2026-07-24 03:29:00 +00:00
Sebastian a30837a130 Merge pull request 'chore(deps): update mongo docker tag to v8' (#63) from renovate/mongo-8.x into main
Reviewed-on: #63
2026-07-24 03:28:51 +00:00
Sebastian 0a43bb8539 fix(deps): update dependency pinia to v4
JS Unit Tests / test (pull_request) Failing after 36s
Build Test / test (pull_request) Successful in 41s
PHP Integration Tests / Integration Tests (pull_request) Successful in 51s
PHP Unit Tests / test (pull_request) Successful in 54s
2026-07-24 03:24:21 +00:00
Sebastian c84924a5a1 chore(deps): update mongo docker tag to v8
Build Test / test (pull_request) Failing after 44s
JS Unit Tests / test (pull_request) Failing after 42s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m24s
PHP Unit Tests / test (pull_request) Successful in 1m40s
2026-07-24 03:24:18 +00:00
Sebastian 4d7844e90a Merge pull request 'chore(deps): update dependency vuetify to v4.1.6' (#55) from renovate/vuetify-4.x-lockfile into main
Reviewed-on: #55
2026-07-24 03:14:21 +00:00
Sebastian e08f37b2cc Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.8' (#57) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #57
2026-07-24 03:14:12 +00:00
Sebastian e8495800e6 Merge pull request 'chore(deps): update dependency vite to v8.1.5' (#58) from renovate/vite-8.x-lockfile into main
Reviewed-on: #58
2026-07-24 03:14:05 +00:00
Sebastian cbcb6d1dcb Merge pull request 'chore(deps): update dependency vue to v3.5.40' (#59) from renovate/vue-monorepo into main
Reviewed-on: #59
2026-07-24 03:12:42 +00:00
Sebastian 3d40bfdf76 Merge pull request 'chore(deps): update dependency vue-tsc to v3.3.8' (#60) from renovate/vue-language-tools-monorepo into main
Reviewed-on: #60
2026-07-24 03:12:35 +00:00
Sebastian a3cfb4f40d Merge pull request 'chore(deps): update dependency typescript to v7' (#54) from renovate/typescript-7.x into main
Reviewed-on: #54
2026-07-24 03:12:19 +00:00
Sebastian 27ddd22a2d Merge pull request 'chore(deps): update vitest monorepo to v4.1.10' (#52) from renovate/vitest-monorepo into main
Reviewed-on: #52
2026-07-24 03:12:04 +00:00
Sebastian 8440a3daec Merge pull request 'chore(deps): update dependency vue-router to v5.2.0' (#61) from renovate/vue-router-5.x-lockfile into main
Reviewed-on: #61
2026-07-24 03:11:56 +00:00
Sebastian 39ab6cf745 chore(deps): update dependency typescript to v7
Build Test / test (pull_request) Successful in 39s
JS Unit Tests / test (pull_request) Failing after 37s
PHP Unit Tests / test (pull_request) Successful in 1m34s
PHP Integration Tests / Integration Tests (pull_request) Successful in 2m45s
2026-07-24 02:55:38 +00:00
Sebastian 0b71c51dd5 chore(deps): update dependency vue-router to v5.2.0
JS Unit Tests / test (pull_request) Failing after 43s
Build Test / test (pull_request) Successful in 44s
PHP Unit Tests / test (pull_request) Successful in 1m29s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m51s
2026-07-24 02:55:33 +00:00
Sebastian 6094dd4cd1 chore(deps): update vitest monorepo to v4.1.10
Build Test / test (pull_request) Successful in 53s
JS Unit Tests / test (pull_request) Failing after 54s
PHP Unit Tests / test (pull_request) Successful in 1m36s
PHP Integration Tests / Integration Tests (pull_request) Successful in 2m32s
2026-07-24 02:55:28 +00:00
Sebastian ee74991175 chore(deps): update dependency vuetify to v4.1.6
Build Test / test (pull_request) Successful in 47s
JS Unit Tests / test (pull_request) Failing after 38s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m36s
PHP Unit Tests / test (pull_request) Successful in 1m57s
2026-07-24 02:55:23 +00:00
Sebastian f7596976da chore(deps): update dependency vue-tsc to v3.3.8
Build Test / test (pull_request) Successful in 42s
JS Unit Tests / test (pull_request) Failing after 42s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m57s
PHP Unit Tests / test (pull_request) Successful in 2m6s
2026-07-24 02:55:20 +00:00
Sebastian b0f388161a chore(deps): update dependency vue to v3.5.40
Build Test / test (pull_request) Failing after 43s
JS Unit Tests / test (pull_request) Failing after 44s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m37s
PHP Unit Tests / test (pull_request) Successful in 1m35s
2026-07-24 02:55:17 +00:00
Sebastian df1ed31ef9 chore(deps): update dependency vite to v8.1.5
Build Test / test (pull_request) Successful in 51s
JS Unit Tests / test (pull_request) Failing after 54s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m27s
PHP Unit Tests / test (pull_request) Successful in 2m5s
2026-07-24 02:55:14 +00:00
Sebastian 0b8c50b93f chore(deps): update dependency @vitejs/plugin-vue to v6.0.8
JS Unit Tests / test (pull_request) Failing after 57s
Build Test / test (pull_request) Successful in 58s
PHP Integration Tests / Integration Tests (pull_request) Successful in 2m4s
PHP Unit Tests / test (pull_request) Successful in 2m24s
2026-07-24 02:55:12 +00:00
Sebastian 6061be119c Merge pull request 'chore: implement php test suites' (#56) from chore/php83-test-suites into main
Reviewed-on: #56
2026-07-24 02:43:42 +00:00
Sebastian 69fc2054d8 fix: install module dependencies before running integration tests
Build Test / test (pull_request) Successful in 28s
JS Unit Tests / test (pull_request) Failing after 29s
PHP Unit Tests / test (pull_request) Successful in 1m11s
PHP Integration Tests / Integration Tests (pull_request) Successful in 1m55s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-23 22:32:06 -04:00
Sebastian cbc2fcd7c7 chore: implement php test suites
Build Test / test (pull_request) Successful in 30s
JS Unit Tests / test (pull_request) Failing after 28s
PHP Unit Tests / test (pull_request) Successful in 1m3s
PHP Integration Tests / Integration Tests (pull_request) Failing after 1m4s
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-23 22:20:36 -04:00
Sebastian c6339ad084 fix: automatically enable discovered account
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-19 18:20:03 -04:00
Sebastian c736e3ebf9 feat: create system user accounts
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-19 06:55:01 -04:00
Sebastian 681e059870 refactor: rename main entry point
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-07-14 17:40:17 -04:00
20 changed files with 1698 additions and 834 deletions
@@ -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
+1 -5
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+25 -6
View File
@@ -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
View File
@@ -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);
}
+5
View File
@@ -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'
],
];
}
+1150 -409
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -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"
}
}
+7 -3
View File
@@ -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')
+13 -4
View File
@@ -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
View File
@@ -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
+19 -17
View File
@@ -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
View File
@@ -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
+30
View File
@@ -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)
})
})
+33
View File
@@ -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/**',
],
},
},
})
+29
View File
@@ -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));
}
}
+10
View File
@@ -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>