diff --git a/internal/sync/api/client_test.go b/internal/sync/api/client_test.go index ffe994f59..7fff01351 100644 --- a/internal/sync/api/client_test.go +++ b/internal/sync/api/client_test.go @@ -528,6 +528,25 @@ func TestClientPatchesSyncManifest(t *testing.T) { }`, string(request.Body)) } +// LaunchDarkly answers a project and a source that no sync wrote yet with an +// empty manifest that has no formatVersion. That is an empty manifest, not +// an unsupported one. +func TestClientAcceptsAnUnwrittenSyncManifest(t *testing.T) { + transport := &recordingClient{Responses: [][]byte{[]byte(`{"items":[],"source":"git:example.com/acme/repo"}`)}} + client := NewClient(transport, "token", "https://example.com") + + manifest, err := client.GetSyncManifest("project", "git:example.com/acme/repo") + + require.NoError(t, err) + assert.Empty(t, manifest.Items) + assert.True(t, manifest.Unwritten()) + + transport = &recordingClient{Responses: [][]byte{[]byte(`{"items":[{"resourceKind":"variation","resourceLookupKey":"support/helpful","fingerprint":"sha256:abc","version":1}],"source":"git:example.com/acme/repo"}`)}} + client = NewClient(transport, "token", "https://example.com") + _, err = client.GetSyncManifest("project", "git:example.com/acme/repo") + require.ErrorContains(t, err, "unsupported formatVersion 0", "entries without a format version are not an unwritten manifest") +} + func TestClientRejectsUnknownSyncManifestVersion(t *testing.T) { transport := &recordingClient{Responses: [][]byte{[]byte(`{ "formatVersion": 99, diff --git a/internal/sync/api/manifest.go b/internal/sync/api/manifest.go index 84fe6207e..417ec4442 100644 --- a/internal/sync/api/manifest.go +++ b/internal/sync/api/manifest.go @@ -113,11 +113,20 @@ func (client Client) PatchSyncManifest( return manifest, nil } +// Unwritten reports whether the manifest is the one that LaunchDarkly returns +// for a project and a source that no sync wrote yet: no entries and no format +// version. Sync treats it as an empty manifest of the supported format. +func (manifest SyncManifest) Unwritten() bool { + return manifest.FormatVersion == 0 && len(manifest.Items) == 0 +} + +// validateSyncManifest accepts a manifest of the supported format, and an +// unwritten manifest, which has no format to check. func validateSyncManifest(manifest SyncManifest) error { - if manifest.FormatVersion != SyncManifestFormatVersion { - return fmt.Errorf("unsupported formatVersion %d", manifest.FormatVersion) + if manifest.Unwritten() || manifest.FormatVersion == SyncManifestFormatVersion { + return nil } - return nil + return fmt.Errorf("unsupported formatVersion %d", manifest.FormatVersion) } func manifestPath(projectKey string) []string { diff --git a/internal/sync/manifest/store.go b/internal/sync/manifest/store.go index 8b3dc7a92..43b7fe4e8 100644 --- a/internal/sync/manifest/store.go +++ b/internal/sync/manifest/store.go @@ -133,7 +133,9 @@ func (store Store) patch( } func (store Store) resourcesFromRemote(projectKey string, remote syncapi.SyncManifest) ([]Resource, error) { - if remote.FormatVersion != syncapi.SyncManifestFormatVersion { + // A project and a source that no sync wrote yet have an empty manifest + // without a format version. + if !remote.Unwritten() && remote.FormatVersion != syncapi.SyncManifestFormatVersion { return nil, fmt.Errorf( "sync manifest for project %q has unsupported formatVersion %d", projectKey, diff --git a/internal/sync/manifest/store_test.go b/internal/sync/manifest/store_test.go index c4513641e..bb952e8da 100644 --- a/internal/sync/manifest/store_test.go +++ b/internal/sync/manifest/store_test.go @@ -49,6 +49,39 @@ func (client *manifestClient) PatchSyncManifest( return response, nil } +// A project that no sync wrote yet has an empty manifest without a format +// version in LaunchDarkly. It loads as no entries, and a manifest with +// entries still needs the supported format. +func TestStoreLoadsAnUnwrittenManifestAsEmpty(t *testing.T) { + client := &manifestClient{manifests: map[string]syncapi.SyncManifest{ + "fresh": {Source: "git:example/repo"}, + "written": { + FormatVersion: syncapi.SyncManifestFormatVersion, + Source: "git:example/repo", + Items: []syncapi.SyncManifestResource{{ + ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/a", + Fingerprint: fingerprint("a"), Version: 2, + }}, + }, + }} + + loaded, err := NewStore(client, "git:example/repo").Load([]string{"fresh", "written"}) + + require.NoError(t, err) + require.Len(t, loaded.Resources, 1) + assert.Equal(t, "written", loaded.Resources[0].ProjectKey) + + client.manifests["fresh"] = syncapi.SyncManifest{ + Source: "git:example/repo", + Items: []syncapi.SyncManifestResource{{ + ResourceKind: syncdomain.KindVariation, ResourceLookupKey: "config/b", + Fingerprint: fingerprint("b"), Version: 1, + }}, + } + _, err = NewStore(client, "git:example/repo").Load([]string{"fresh"}) + require.ErrorContains(t, err, "unsupported formatVersion 0") +} + func TestStoreLoadsProjectManifests(t *testing.T) { client := &manifestClient{manifests: map[string]syncapi.SyncManifest{ "zeta": {