|
| 1 | +import { describe, expect, test } from 'bun:test' |
| 2 | +import { compileTopicPattern } from './matcher.js' |
| 3 | + |
| 4 | +describe('compileTopicPattern.match (publish/dispatch path)', () => { |
| 5 | + test('literal pattern 必须完全相等', () => { |
| 6 | + const c = compileTopicPattern('devices/online') |
| 7 | + expect(c.match('devices/online')).toEqual({}) |
| 8 | + expect(c.match('devices/offline')).toBeNull() |
| 9 | + expect(c.match('devices/online/extra')).toBeNull() |
| 10 | + }) |
| 11 | + |
| 12 | + test(':name 命名参数会提取值', () => { |
| 13 | + const c = compileTopicPattern('devices/:uid/events') |
| 14 | + expect(c.match('devices/abc/events')).toEqual({ uid: 'abc' }) |
| 15 | + expect(c.match('devices/abc/other')).toBeNull() |
| 16 | + expect(c.match('devices/abc')).toBeNull() |
| 17 | + }) |
| 18 | + |
| 19 | + test('* catch-all 会把剩余部分收集到 params["*"]', () => { |
| 20 | + const c = compileTopicPattern('files/*') |
| 21 | + expect(c.match('files/a')).toEqual({ '*': 'a' }) |
| 22 | + expect(c.match('files/a/b/c')).toEqual({ '*': 'a/b/c' }) |
| 23 | + expect(c.match('files')).toEqual({ '*': '' }) |
| 24 | + expect(c.match('other/a')).toBeNull() |
| 25 | + }) |
| 26 | + |
| 27 | + test(':name 与 * 可以混用', () => { |
| 28 | + const c = compileTopicPattern('users/:uid/*') |
| 29 | + expect(c.match('users/alice/profile/avatar')).toEqual({ uid: 'alice', '*': 'profile/avatar' }) |
| 30 | + expect(c.match('users/alice')).toEqual({ uid: 'alice', '*': '' }) |
| 31 | + }) |
| 32 | + |
| 33 | + test('* 出现在非末尾抛错', () => { |
| 34 | + expect(() => compileTopicPattern('a/*/b')).toThrow(/Catch-all wildcard/) |
| 35 | + }) |
| 36 | + |
| 37 | + test('空 :param 名抛错', () => { |
| 38 | + expect(() => compileTopicPattern('a/:')).toThrow(/parameter name cannot be empty/) |
| 39 | + }) |
| 40 | + |
| 41 | + test('MQTT 通配符 + / # 在 pattern 里被当成 literal,不再生效', () => { |
| 42 | + const c = compileTopicPattern('devices/+/events') |
| 43 | + // '+' 不再是单层通配符,只能字面匹配 |
| 44 | + expect(c.match('devices/abc/events')).toBeNull() |
| 45 | + expect(c.match('devices/+/events')).toEqual({}) |
| 46 | + }) |
| 47 | +}) |
| 48 | + |
| 49 | +describe('compileTopicPattern.matchSubscription (subscribe path)', () => { |
| 50 | + test('精确订阅与 publish-path 行为一致', () => { |
| 51 | + const c = compileTopicPattern('devices/:uid/events') |
| 52 | + expect(c.matchSubscription('devices/abc/events')).toEqual({ uid: 'abc' }) |
| 53 | + expect(c.matchSubscription('devices/abc/other')).toBeNull() |
| 54 | + }) |
| 55 | + |
| 56 | + test('订阅中的 + 匹配单层但不绑定 param', () => { |
| 57 | + const c = compileTopicPattern('devices/:uid/events') |
| 58 | + expect(c.matchSubscription('devices/+/events')).toEqual({}) |
| 59 | + expect(c.matchSubscription('devices/+/other')).toBeNull() |
| 60 | + }) |
| 61 | + |
| 62 | + test('订阅中的 # 吃掉剩余所有段', () => { |
| 63 | + const c = compileTopicPattern('devices/:uid/events') |
| 64 | + expect(c.matchSubscription('devices/#')).toEqual({}) |
| 65 | + expect(c.matchSubscription('#')).toEqual({}) |
| 66 | + }) |
| 67 | + |
| 68 | + test('订阅与 catch-all pattern 互相兼容', () => { |
| 69 | + const c = compileTopicPattern('files/*') |
| 70 | + expect(c.matchSubscription('files/#')).toEqual({ '*': '' }) |
| 71 | + expect(c.matchSubscription('files/+/a')).toEqual({ '*': '+/a' }) |
| 72 | + expect(c.matchSubscription('files/a/b')).toEqual({ '*': 'a/b' }) |
| 73 | + }) |
| 74 | + |
| 75 | + test('+ 能匹配命名参数所在的段但不会泄露值给 param', () => { |
| 76 | + const c = compileTopicPattern('users/:uid/notifications') |
| 77 | + const params = c.matchSubscription('users/+/notifications') |
| 78 | + expect(params).toEqual({}) |
| 79 | + expect(params).not.toHaveProperty('uid') |
| 80 | + }) |
| 81 | +}) |
0 commit comments