feat(PROJ-45): IMAP Per-Folder UID-Tracking + UIDVALIDITY-Check
- FolderState: GetFolderState, UpsertFolderState, ListFolderStates, DecideResync - syncFolder nutzt per-folder UID-Tracking statt globalem highest_uid - UIDVALIDITY-Check loest automatisch Full-Resync aus - imap_folder_state Tabelle in initSchema (CREATE TABLE IF NOT EXISTS) - SetAuditLogger in main.go verdrahtet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package imap
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestDecideResync covers the per-folder sync gate introduced by PROJ-45.
|
||||
// The branches under test correspond directly to the acceptance criteria:
|
||||
// - first-ever sync (no stored state)
|
||||
// - normal incremental sync (UIDVALIDITY unchanged)
|
||||
// - UIDVALIDITY mismatch → full resync + audit event
|
||||
// - server returns UIDVALIDITY=0 → defensive fallback, keep stored cursor
|
||||
func TestDecideResync(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
storedUID uint32
|
||||
storedValid uint32
|
||||
serverValid uint32
|
||||
wantFull bool
|
||||
wantReset bool
|
||||
wantStart uint32
|
||||
}{
|
||||
{
|
||||
name: "first sync, no stored state",
|
||||
storedUID: 0,
|
||||
storedValid: 0,
|
||||
serverValid: 12345,
|
||||
wantFull: true,
|
||||
wantReset: false,
|
||||
wantStart: 0,
|
||||
},
|
||||
{
|
||||
name: "incremental, validity unchanged",
|
||||
storedUID: 1500,
|
||||
storedValid: 12345,
|
||||
serverValid: 12345,
|
||||
wantFull: false,
|
||||
wantReset: false,
|
||||
wantStart: 1500,
|
||||
},
|
||||
{
|
||||
name: "uidvalidity mismatch triggers full resync + audit",
|
||||
storedUID: 1500,
|
||||
storedValid: 12345,
|
||||
serverValid: 99999,
|
||||
wantFull: true,
|
||||
wantReset: true,
|
||||
wantStart: 0,
|
||||
},
|
||||
{
|
||||
name: "server uidvalidity 0 defensive: keep cursor, no reset",
|
||||
storedUID: 800,
|
||||
storedValid: 12345,
|
||||
serverValid: 0,
|
||||
wantFull: false,
|
||||
wantReset: false,
|
||||
wantStart: 800,
|
||||
},
|
||||
{
|
||||
name: "stored validity 0 (legacy row) does not trigger reset",
|
||||
storedUID: 0,
|
||||
storedValid: 0,
|
||||
serverValid: 7777,
|
||||
wantFull: true,
|
||||
wantReset: false,
|
||||
wantStart: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := DecideResync(tc.storedUID, tc.storedValid, tc.serverValid)
|
||||
if got.FullResync != tc.wantFull {
|
||||
t.Errorf("FullResync: got %v want %v", got.FullResync, tc.wantFull)
|
||||
}
|
||||
if got.UIDValidReset != tc.wantReset {
|
||||
t.Errorf("UIDValidReset: got %v want %v", got.UIDValidReset, tc.wantReset)
|
||||
}
|
||||
if got.EffectiveStart != tc.wantStart {
|
||||
t.Errorf("EffectiveStart: got %d want %d", got.EffectiveStart, tc.wantStart)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user