Compare commits

...

2 Commits

Author SHA1 Message Date
mtkell f9c5ef36da Merge pull request 'Implement WooCommerce publishDraft flow' (#2) from feature/wp-publish-draft into main
Publish Docs / publish-docs (push) Successful in 1m5s Details
Reviewed-on: #2
## publishDraft Implementation Review

### 1. Cleanup Suggestions

**None.** After thorough review:

- **Production code** (39 lines total): Clean, minimal, correct. The `publish_status.dart` import is used by `updateProductStatus`. No dead code, no unused imports. `flutter analyze` reports zero issues.
- **Test code**: The `publishDraft` and `updateProductStatus` groups each have their own local JSON builder helper. The slight duplication is intentional — each is scoped to its group with the right defaults. No cleanup needed.
- **Static analysis**: Both `feature_wordpress` and `kell_web` pass `flutter analyze` with zero issues.
- **Dashboard test stub** (`_StubProductPublishingRepository.publishDraft` still throws `UnimplementedError`): Correct — the dashboard never calls `publishDraft`, so the stub is never exercised. No change needed.

### 2. Test Gaps Worth Adding (optional, not blocking)

| Gap | Priority | Rationale |
|-----|----------|-----------|
| `publishDraft` with 404 (product not found) | Low | Already covered implicitly by `WooCommerceApiClient.updateProduct` error handling, but an explicit 404 test would document the "missing product" scenario |
| `publishDraft` verifies auth header is present | Low | Auth is tested at the `WooCommerceApiClient` level already; adding it here would be redundant |
| `publishDraft` on already-published product (idempotency) | Low | Tests WooCommerce behavior, not our code |

None of these are blocking.

### 3. PR Readiness

** Ready for merge.**

Evidence:
- `flutter analyze` — 0 issues on both packages
- `flutter test` — 99 pass (feature_wordpress), 24 pass (kell_web)
- 2 files changed, ~15 lines of net production code
- No architectural changes, no composition changes, no credential exposure
- Strict package boundaries preserved
- Runtime config via `--dart-define` unchanged
2026-04-06 00:20:50 +00:00
Mike Kell ed2333d188 Implement WooCommerce publishDraft flow
Validate Docs / validate-docs (push) Successful in 1m24s Details
2026-04-05 20:15:00 -04:00
2 changed files with 101 additions and 23 deletions

View File

@ -6,10 +6,8 @@ import 'wordpress_product_mapper.dart';
/// Real [ProductPublishingRepository] backed by the WooCommerce REST API.
///
/// Supports read-only product retrieval and status updates via
/// `PUT /wp-json/wc/v3/products/{id}`. The [publishDraft] method throws
/// [UnimplementedError] full publishing support will be added in a future
/// iteration.
/// Supports product retrieval, publishing, and status updates via
/// `PUT /wp-json/wc/v3/products/{id}`.
class WordPressProductPublishingRepository implements ProductPublishingRepository {
final WooCommerceApiClient _apiClient;
final WordPressProductMapper _mapper;
@ -27,14 +25,9 @@ class WordPressProductPublishingRepository implements ProductPublishingRepositor
}
@override
Future<ProductDraft> publishDraft(String id) {
// Publishing is not yet implemented for the real backend.
// This will be added in a future iteration alongside media upload
// and order sync.
throw UnimplementedError(
'WordPressProductPublishingRepository.publishDraft is not yet implemented. '
'Use FakeProductPublishingRepository for development.',
);
Future<ProductDraft> publishDraft(String id) async {
final json = await _apiClient.updateProduct(id, {'status': 'publish'});
return _mapper.fromJson(json);
}
@override

View File

@ -120,9 +120,66 @@ void main() {
expect(drafts[1].name, 'Product 2');
});
test('publishDraft throws UnimplementedError', () {
group('publishDraft', () {
/// Builds a single WooCommerce product JSON response with the given
/// [id] and status set to `'publish'`.
Map<String, dynamic> buildPublishedProductJson(int id) {
return {
'id': id,
'name': 'Product $id',
'description': '<p>Description</p>',
'price': '10.99',
'sku': 'SKU-$id',
'status': 'publish',
'date_modified': '2026-04-01T10:00:00',
'date_created': '2026-03-01T08:00:00',
'categories': [
{'id': 1, 'name': 'Test Category', 'slug': 'test-category'},
],
'images': [
{'id': 1, 'src': 'https://example.com/img$id.jpg'},
],
};
}
test('sends PUT with status publish and returns mapped ProductDraft', () async {
Map<String, dynamic>? capturedBody;
Uri? capturedUri;
String? capturedMethod;
final mockClient = MockClient((request) async {
return http.Response('[]', 200);
capturedMethod = request.method;
capturedUri = request.url;
capturedBody = jsonDecode(request.body) as Map<String, dynamic>;
return http.Response(jsonEncode(buildPublishedProductJson(42)), 200);
});
final apiClient = WooCommerceApiClient(
siteUrl: 'https://store.example.com',
consumerKey: 'ck_test',
consumerSecret: 'cs_test',
httpClient: mockClient,
);
final repository = WordPressProductPublishingRepository(apiClient: apiClient);
final result = await repository.publishDraft('42');
// Verify the HTTP request shape.
expect(capturedMethod, 'PUT');
expect(capturedUri!.path, '/wp-json/wc/v3/products/42');
expect(capturedBody, {'status': 'publish'});
// Verify the returned domain object is cleanly mapped.
expect(result, isA<ProductDraft>());
expect(result.id, '42');
expect(result.name, 'Product 42');
expect(result.status, PublishStatus.published);
});
test('propagates WooCommerceApiException on non-200 response', () async {
final mockClient = MockClient((request) async {
return http.Response('{"code":"rest_cannot_edit"}', 403);
});
final apiClient = WooCommerceApiClient(
@ -134,7 +191,35 @@ void main() {
final repository = WordPressProductPublishingRepository(apiClient: apiClient);
expect(() => repository.publishDraft('1'), throwsA(isA<UnimplementedError>()));
expect(() => repository.publishDraft('42'), throwsA(isA<WooCommerceApiException>()));
});
test('returns ProductDraft, not raw WooCommerce JSON (package boundary)', () async {
final mockClient = MockClient((request) async {
final json = buildPublishedProductJson(9);
json['permalink'] = 'https://store.example.com/product/9';
json['meta_data'] = [
{'id': 1, 'key': '_internal', 'value': 'secret'},
];
return http.Response(jsonEncode(json), 200);
});
final apiClient = WooCommerceApiClient(
siteUrl: 'https://store.example.com',
consumerKey: 'ck_test',
consumerSecret: 'cs_test',
httpClient: mockClient,
);
final repository = WordPressProductPublishingRepository(apiClient: apiClient);
final result = await repository.publishDraft('9');
// The result is a domain ProductDraft no WooCommerce-specific
// fields like permalink or meta_data are exposed.
expect(result, isA<ProductDraft>());
expect(result.id, '9');
expect(result.status, PublishStatus.published);
});
});
test('getProductDrafts propagates API errors', () async {