Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,47 @@ The CI runs on multiple Dart versions (3.5.0, stable, beta) and OS (Ubuntu, Wind
- `chore: Update dependencies to latest versions`

### Testing Guidelines
- Follow Given-When-Then test descriptions
- **Follow Given-When-Then pattern**: Test descriptions should generally follow the Given-When-Then (GWT) pattern for clarity
- **Flexible structure**: Depending on context, you can structure tests in different ways:
- Combine Given and When in group descriptions when they share setup (e.g., `group('Given a NewContext, when withRequest is called with a new Request,', () { ... })`)
- Split Given and When into separate nested groups when it improves organization
- Include all three parts (Given-When-Then) in a single test description for simple cases
- Combine When and Then when the action and assertion are closely related
- **Shared setup**: When tests in a group share the same action, consider executing that action in a `setUp` block to reduce duplication
- **Single responsibility**: Each test should validate a single requirement or assertion when it improves clarity, but multiple related assertions in one test are acceptable
- **Clear test titles**: Use descriptive test names that make the intent and validation clear
- Use Arrange-Act-Assert pattern in test bodies
- Tests are in `test/` directory mirroring `lib/` structure
- Run specific test files: `dart test test/router/router_test.dart`
- All tests should pass; errors in test output are expected test scenarios

**Example test structure (one approach):**
```dart
group('Given a NewContext, when withRequest is called with a new Request,', () {
late NewContext context;
late Request newRequest;
late NewContext newContext;

setUp(() {
// Arrange
context = Request(Method.get, Uri.parse('http://test.com')).toContext(Object());
newRequest = Request(Method.post, Uri.parse('http://test.com/new'));
// Act (shared action for all tests in this group)
newContext = context.withRequest(newRequest);
});

test('then it returns a NewContext instance', () {
// Assert
expect(newContext, isA<NewContext>());
});

test('then the new context contains the new request', () {
// Assert
expect(newContext.request, same(newRequest));
});
});
```

### Common Development Patterns
- **Handlers**: Functions that process RequestContext and return ResponseContext
- **Middleware**: Functions that wrap handlers to add functionality (logging, routing, etc.)
Expand Down
18 changes: 18 additions & 0 deletions lib/src/adapter/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ final class NewContext extends RequestContext
@override
ResponseContext respond(final Response r) =>
ResponseContext._(request, token, r);

/// Creates a new [NewContext] with a different [Request] while preserving
/// the same [token].
///
/// This is a convenience method for middleware that needs to rewrite a
/// request before passing it to the inner handler. Instead of the low-level
/// pattern:
/// ```dart
/// final rewrittenRequest = req.copyWith(requestedUri: newRequested);
/// return await inner(rewrittenRequest.toContext(ctx.token));
/// ```
///
/// You can use the more readable pattern:
/// ```dart
/// final rewrittenRequest = req.copyWith(requestedUri: newRequested);
/// return await inner(ctx.withRequest(rewrittenRequest));
/// ```
NewContext withRequest(final Request req) => NewContext._(req, token);
}

/// A sealed base class for contexts that represent a handled request.
Expand Down
112 changes: 112 additions & 0 deletions test/src/adapter/context_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'package:relic/relic.dart';
import 'package:relic/src/adapter/context.dart';
import 'package:test/test.dart';

void main() {
group('Given a NewContext, when withRequest is called with a new Request,',
() {
late NewContext context;
late Request originalRequest;
late Request newRequest;
late NewContext newContext;
late Object token;

setUp(() {
originalRequest = Request(Method.get, Uri.parse('http://test.com/path'));
token = Object();
context = originalRequest.toContext(token);
newRequest = Request(Method.post, Uri.parse('http://test.com/newpath'));
newContext = context.withRequest(newRequest);
});

test('then it returns a NewContext instance', () {
expect(newContext, isA<NewContext>());
});

test('then the new context contains the new request', () {
expect(newContext.request, same(newRequest));
});

test('then the new context preserves the same token', () {
expect(newContext.token, same(token));
});

test('then the new context is not the same instance as the original', () {
expect(newContext, isNot(same(context)));
});

test('then the original context remains unchanged', () {
expect(context.request, same(originalRequest));
expect(context.token, same(token));
});

test('then the new context can transition to ResponseContext', () {
final responseContext =
newContext.respond(Response.ok(body: Body.fromString('test')));

expect(responseContext, isA<ResponseContext>());
expect(responseContext.request, same(newRequest));
expect(responseContext.token, same(token));
});

test('then the new context can transition to HijackContext', () {
final hijackContext = newContext.hijack((final channel) {});

expect(hijackContext, isA<HijackContext>());
expect(hijackContext.request, same(newRequest));
expect(hijackContext.token, same(token));
});

test('then the new context can transition to ConnectContext', () {
final connectContext = newContext.connect((final webSocket) {});

expect(connectContext, isA<ConnectContext>());
expect(connectContext.request, same(newRequest));
expect(connectContext.token, same(token));
});
});

group(
'Given a NewContext, when withRequest is called with a request created using copyWith,',
() {
late NewContext context;
late Request originalRequest;
late Object token;

setUp(() {
originalRequest = Request(Method.get, Uri.parse('http://test.com/path'));
token = Object();
context = originalRequest.toContext(token);
});

test('then it simplifies middleware request rewriting pattern', () {
final rewrittenRequest = originalRequest.copyWith(
requestedUri: Uri.parse('http://test.com/rewritten'),
);
final newContext = context.withRequest(rewrittenRequest);

expect(newContext, isA<NewContext>());
expect(newContext.request.requestedUri,
Uri.parse('http://test.com/rewritten'));
expect(newContext.token, same(token));
});

test('then it maintains the same token across multiple transformations',
() {
final request1 = originalRequest.copyWith(
requestedUri: Uri.parse('http://test.com/step1'),
);
final context1 = context.withRequest(request1);

final request2 = request1.copyWith(
requestedUri: Uri.parse('http://test.com/step2'),
);
final context2 = context1.withRequest(request2);

expect(context.token, same(token));
expect(context1.token, same(token));
expect(context2.token, same(token));
expect(context2.request.requestedUri, Uri.parse('http://test.com/step2'));
});
});
}
Loading