Files
com_r35157_nenjim-hubd-impl…/openspec/specs/notification-services/spec.md
T

115 lines
8.4 KiB
Markdown

# notification-services Specification
## Purpose
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
## Requirements
### Requirement: Transport-independent bound notification contract
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
#### Scenario: Bound consumer sends a message
- **WHEN** a consumer receives a configured `BoundNotificationService`
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
#### Scenario: Delivery failure is reported synchronously
- **WHEN** a bound transport encounters a delivery failure
- **THEN** `push(message)` reports the failure through its `IOException` contract
### Requirement: Addressed services bind destination configuration
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
#### Scenario: Destination is bound once
- **WHEN** a destination is passed to an addressed notification service's binding operation
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
### Requirement: Notification message validation
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
#### Scenario: Null message is rejected
- **WHEN** a caller pushes a null notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
#### Scenario: Empty or blank message is rejected
- **WHEN** a caller pushes an empty or whitespace-only notification message
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
### Requirement: Pushover delivery remains functional
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
#### Scenario: Valid Pushover message is submitted
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
- **THEN** the service submits the same message through the existing Pushover API request flow
#### Scenario: Pushover rejects delivery
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
- **THEN** the service reports the delivery failure as an `IOException`
### Requirement: SMTP binding captures address and subject
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
#### Scenario: SMTP service is fully bound
- **WHEN** a caller binds a valid email address and subject
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
#### Scenario: Invalid SMTP binding is rejected
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
- **THEN** the operation throws `IllegalArgumentException`
#### Scenario: Valid SMTP delivery remains unimplemented
- **WHEN** a valid message is pushed through a valid SMTP binding
- **THEN** the service reports that SMTP delivery is not implemented
### Requirement: Discord webhook delivery
The Discord notification service SHALL remain a `BoundNotificationService` that is fully configured at construction from a non-null structurally valid canonical Discord incoming-webhook `ΩRestEndpointΩ` and a non-null positive `Duration`. It SHALL make no construction-time network request, follow no HTTP redirects, and SHALL NOT expose a raw `URL` or raw `String` constructor.
The endpoint SHALL be an absolute HTTPS URI with a case-insensitive `discord.com` host, no user-info, fragment, query, or non-HTTPS port, and either an unversioned `/api/webhooks/{id}/{token}` path or a versioned `/api/v{version}/webhooks/{id}/{token}` path using a non-discontinued Discord API version from 6 through 10, with non-blank ID and token segments. Constructor validation diagnostics SHALL NOT disclose the endpoint or token.
Every public `push(...)` entry point SHALL reject a null, empty, blank, or over-2,000-character `ΩNotificationMessageΩ` with `IllegalArgumentException` before JSON serialization, request construction, or transport work. For every valid message, the service SHALL synchronously send exactly one POST to the configured endpoint with `wait=true`, the configured request timeout, JSON UTF-8 content and accept headers, a `DiscordBot (url, version)` User-Agent identifying Nenjim, and a safely serialized JSON object whose `content` preserves the message exactly. The webhook token SHALL be the only authentication mechanism.
Every 2xx HTTP response SHALL return normally. Every non-2xx response, including redirects and 429 responses, and every communication or request-preparation failure SHALL cause `IOException` without retrying, waiting, or sending another request. Response failures SHALL include the HTTP status and useful response body when available, without disclosing the endpoint or token. When synchronous HTTP delivery is interrupted, the service SHALL restore the thread interrupt flag and throw an `IOException` retaining the `InterruptedException` as its cause.
#### Scenario: Fully configured construction
- **WHEN** a caller constructs the service with a canonical Discord webhook endpoint and positive timeout
- **THEN** it receives a bound notification service without a network request
#### Scenario: Invalid endpoint or timeout is rejected
- **WHEN** a caller supplies a null or structurally invalid endpoint, or a null, zero, or negative timeout
- **THEN** construction throws `IllegalArgumentException` without disclosing the endpoint token
#### Scenario: Invalid notification message is rejected before delivery
- **WHEN** a caller pushes a null, empty, blank, or over-2,000-character message
- **THEN** the service throws `IllegalArgumentException` before serialization or HTTP work
#### Scenario: Valid message is synchronously confirmed
- **WHEN** a caller pushes a valid notification message and Discord returns a 2xx response
- **THEN** the service sends one synchronous JSON POST with `wait=true` and returns normally
#### Scenario: Discord rejects delivery
- **WHEN** Discord returns a non-2xx response, including 429 or a redirect
- **THEN** the service throws `IOException` containing the status and does not retry or follow the redirect
#### Scenario: Delivery communication fails
- **WHEN** preparing or sending the webhook request fails
- **THEN** the service throws `IOException` without exposing the webhook endpoint or token
#### Scenario: Delivery is interrupted
- **WHEN** synchronous webhook delivery is interrupted
- **THEN** the service restores the thread interrupt status and throws `IOException` with the interruption as its cause
#### Scenario: Webhook secret remains protected
- **WHEN** validation or delivery reports an error
- **THEN** no log or exception message includes the complete webhook endpoint or token
### Requirement: Obsolete notification API is removed
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
#### Scenario: Production code uses the new service API
- **WHEN** the production source tree is inspected after migration
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
#### Scenario: Removed types are unavailable
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
- **THEN** that type is no longer part of the source or compiled API