Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

- make `httpOnly`, `secure`, and `partitioned` two-state flags with `false` defaults
- treat omitted flags and explicit `false` as the same value semantics
Comment on lines +3 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document the clear API break explicitly.

Lines 3-4 describe the semantic shift, but the compile-time break most consumers will hit is that CookieNullableField.httpOnly, CookieNullableField.secure, and CookieNullableField.partitioned are gone. Please call that out here so the migration path is discoverable from the release notes.

✏️ Suggested changelog wording
-- make `httpOnly`, `secure`, and `partitioned` two-state flags with `false` defaults
-- treat omitted flags and explicit `false` as the same value semantics
+- BREAKING: make `httpOnly`, `secure`, and `partitioned` two-state flags with `false` defaults
+- BREAKING: treat omitted flags and explicit `false` as the same value semantics
+- BREAKING: remove `CookieNullableField.httpOnly`, `CookieNullableField.secure`, and
+  `CookieNullableField.partitioned`; `copyWith(clear: ...)` can no longer clear those flags
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 3 - 4, Update the changelog entry to explicitly
document the breaking API change: state that the properties
CookieNullableField.httpOnly, CookieNullableField.secure, and
CookieNullableField.partitioned have been removed (compile-time break) and
describe the migration path (e.g., replace usages with the new two-state flags
on Cookie or set explicit false/omitted semantics accordingly), so consumers
know how to update their code; ensure the wording appears near the existing
lines about flag semantics to make the relationship clear.


## 0.1.0

- add `Cookie.validate` for explicit pre-serialization checks
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ if (errors.isNotEmpty) {
- `SameSite=None` requires `Secure=true`.
- `Partitioned=true` requires `Secure=true`.

## Flag Semantics

- `HttpOnly`, `Secure`, and `Partitioned` are two-state flags.
- Omitting a flag is equivalent to setting it to `false`.

# API Reference

See the [API documentation](https://pub.dev/documentation/ocookie) for detailed information about all available APIs.
Expand Down
48 changes: 16 additions & 32 deletions lib/src/cookie.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,10 @@ enum CookieSameSite {
enum CookieNullableField {
expires,
domain,
httpOnly,
maxAge,
path,
priority,
sameSite,
secure,
partitioned,
}

class Cookie {
Expand All @@ -49,26 +46,26 @@ class Cookie {
this.value, {
this.expires,
this.domain,
this.httpOnly,
this.httpOnly = false,
this.maxAge,
this.path,
this.priority,
this.sameSite,
this.secure,
this.partitioned,
this.secure = false,
this.partitioned = false,
});

final String name;
final String value;
final DateTime? expires;
final String? domain;
final bool? httpOnly;
final bool httpOnly;
final Duration? maxAge;
final String? path;
final CookiePriority? priority;
final CookieSameSite? sameSite;
final bool? secure;
final bool? partitioned;
final bool secure;
final bool partitioned;

Cookie copyWith({
String? name,
Expand Down Expand Up @@ -100,17 +97,10 @@ class Cookie {

assertNoConflict(CookieNullableField.expires, expires, 'expires');
assertNoConflict(CookieNullableField.domain, domain, 'domain');
assertNoConflict(CookieNullableField.httpOnly, httpOnly, 'httpOnly');
assertNoConflict(CookieNullableField.maxAge, maxAge, 'maxAge');
assertNoConflict(CookieNullableField.path, path, 'path');
assertNoConflict(CookieNullableField.priority, priority, 'priority');
assertNoConflict(CookieNullableField.sameSite, sameSite, 'sameSite');
assertNoConflict(CookieNullableField.secure, secure, 'secure');
assertNoConflict(
CookieNullableField.partitioned,
partitioned,
'partitioned',
);

return Cookie(
name ?? this.name,
Expand All @@ -121,9 +111,7 @@ class Cookie {
domain: clear.contains(CookieNullableField.domain)
? null
: domain ?? this.domain,
httpOnly: clear.contains(CookieNullableField.httpOnly)
? null
: httpOnly ?? this.httpOnly,
httpOnly: httpOnly ?? this.httpOnly,
maxAge: clear.contains(CookieNullableField.maxAge)
? null
: maxAge ?? this.maxAge,
Expand All @@ -134,12 +122,8 @@ class Cookie {
sameSite: clear.contains(CookieNullableField.sameSite)
? null
: sameSite ?? this.sameSite,
secure: clear.contains(CookieNullableField.secure)
? null
: secure ?? this.secure,
partitioned: clear.contains(CookieNullableField.partitioned)
? null
: partitioned ?? this.partitioned,
secure: secure ?? this.secure,
partitioned: partitioned ?? this.partitioned,
);
}

Expand Down Expand Up @@ -171,12 +155,12 @@ class Cookie {
if (domain?.isNotEmpty == true && !cookieAllowPattern.hasMatch(domain!)) {
errors.add('domain is invalid');
}
if (sameSite == CookieSameSite.none && secure != true) {
if (sameSite == CookieSameSite.none && !secure) {
errors.add(
'SameSite attribute is set to none, but the secure flag is not set to true.',
);
}
if (partitioned == true && secure != true) {
if (partitioned && !secure) {
errors.add(
'Partitioned attribute is set, but the secure flag is not set to true.',
);
Expand All @@ -202,11 +186,11 @@ class Cookie {
if (domain?.isNotEmpty == true && !cookieAllowPattern.hasMatch(domain!)) {
throw ArgumentError.value(domain, 'domain', 'domain is invalid');
}
if (sameSite == CookieSameSite.none && secure != true) {
if (sameSite == CookieSameSite.none && !secure) {
throw StateError(
'SameSite attribute is set to none, but the secure flag is not set to true.');
}
if (partitioned == true && secure != true) {
if (partitioned && !secure) {
throw StateError(
'Partitioned attribute is set, but the secure flag is not set to true.');
}
Expand All @@ -217,8 +201,8 @@ class Cookie {
if (domain?.isNotEmpty == true) 'Domain=$domain',
if (path?.isNotEmpty == true) 'Path=$path',
if (expires != null) 'Expires=${formatHttpDate(expires!)}',
if (httpOnly == true) 'HttpOnly',
if (secure == true) 'Secure',
if (httpOnly) 'HttpOnly',
if (secure) 'Secure',
if (priority != null)
'Priority=${switch (priority!) {
CookiePriority.low => 'Low',
Expand All @@ -231,7 +215,7 @@ class Cookie {
CookieSameSite.lax => 'Lax',
CookieSameSite.none => 'None',
}}',
if (partitioned == true) 'Partitioned',
if (partitioned) 'Partitioned',
];

return parts.join('; ');
Expand Down
23 changes: 17 additions & 6 deletions test/copy_with_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,22 @@ void main() {
clear: {
CookieNullableField.expires,
CookieNullableField.domain,
CookieNullableField.httpOnly,
CookieNullableField.maxAge,
CookieNullableField.path,
CookieNullableField.priority,
CookieNullableField.sameSite,
CookieNullableField.secure,
CookieNullableField.partitioned,
},
);

expect(result.expires, isNull);
expect(result.domain, isNull);
expect(result.httpOnly, isNull);
expect(result.httpOnly, isTrue);
expect(result.maxAge, isNull);
expect(result.path, isNull);
expect(result.priority, isNull);
expect(result.sameSite, isNull);
expect(result.secure, isNull);
expect(result.partitioned, isNull);
expect(result.secure, isTrue);
expect(result.partitioned, isTrue);
});

test('should throw when a field is set and cleared together', () {
Expand Down Expand Up @@ -107,5 +104,19 @@ void main() {
expect(a, equals(b));
expect(a.hashCode, b.hashCode);
});

test('should treat omitted flags and false flags as equal', () {
final a = Cookie('sid', 'abc');
final b = Cookie(
'sid',
'abc',
httpOnly: false,
secure: false,
partitioned: false,
);

expect(a, equals(b));
expect(a.hashCode, b.hashCode);
});
});
}
68 changes: 68 additions & 0 deletions test/from_string_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,61 @@ void main() {
final cookie = Cookie.fromString('a="hello%20world"');

expect(cookie.value, 'hello world');
expect(cookie.httpOnly, isFalse);
expect(cookie.secure, isFalse);
expect(cookie.partitioned, isFalse);
});

test('should preserve quoted values containing equals signs', () {
final cookie = Cookie.fromString('session="a=b=c%202"');

expect(cookie.value, 'a=b=c 2');
});

test('should ignore unknown attributes from real-world headers', () {
final cookie = Cookie.fromString(
'sid=abc123; Path=/; HttpOnly; Secure; SameSite=None; Foo=bar; Version=1',
);

expect(cookie.name, 'sid');
expect(cookie.value, 'abc123');
expect(cookie.path, '/');
expect(cookie.httpOnly, isTrue);
expect(cookie.secure, isTrue);
expect(cookie.sameSite, CookieSameSite.none);
expect(cookie.priority, isNull);
});

test('should keep the last repeated recognized attribute', () {
final cookie = Cookie.fromString(
'sid=abc; Path=/one; Path=/two; SameSite=Lax; SameSite=None; Secure',
);

expect(cookie.path, '/two');
expect(cookie.sameSite, CookieSameSite.none);
expect(cookie.secure, isTrue);
});

test('should treat valueless flags with non-standard values as enabled',
() {
final cookie = Cookie.fromString(
'sid=abc; Secure=1; HttpOnly=true; Partitioned=?1',
);

expect(cookie.secure, isTrue);
expect(cookie.httpOnly, isTrue);
expect(cookie.partitioned, isTrue);
});

test('should ignore invalid recognized attribute values without failing',
() {
final cookie = Cookie.fromString(
'sid=abc; SameSite=invalid; Priority=urgent; Secure',
);

expect(cookie.sameSite, isNull);
expect(cookie.priority, isNull);
expect(cookie.secure, isTrue);
});

test('should throw for invalid first pair without equals', () {
Expand Down Expand Up @@ -90,5 +145,18 @@ void main() {
'c=d',
]);
});

test('should split combined headers with quoted commas and expires dates',
() {
final values = Cookie.splitSetCookie(
'a="b,c"; Path=/, sid=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; HttpOnly, theme=light',
);

expect(values, [
'a="b,c"; Path=/',
'sid=1; Expires=Wed, 21 Oct 2015 07:28:00 GMT; HttpOnly',
'theme=light',
]);
});
});
}
Loading