Coverage for app/backend/src/couchers/models/ota.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 17:03 +0000

1import enum 

2import uuid 

3from datetime import datetime 

4from typing import TYPE_CHECKING 

5 

6from sqlalchemy import ( 

7 BigInteger, 

8 CheckConstraint, 

9 DateTime, 

10 Enum, 

11 ForeignKey, 

12 Index, 

13 String, 

14 UniqueConstraint, 

15 Uuid, 

16 func, 

17) 

18from sqlalchemy.orm import Mapped, mapped_column, relationship 

19 

20from couchers.models.base import Base 

21 

22if TYPE_CHECKING: 

23 from couchers.models.users import User 

24 

25 

26class OTAPlatform(enum.Enum): 

27 ios = enum.auto() 

28 android = enum.auto() 

29 

30 

31class OTAPackage(Base, kw_only=True): 

32 # The signed manifest bytes live on the CDN under `version`; this row only records which bundle is 

33 # available and how recent it is, so the backend can resolve a request and fetch the bytes verbatim. 

34 __tablename__ = "ota_packages" 

35 

36 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

37 created: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

38 

39 creator_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) 

40 

41 platform: Mapped[OTAPlatform] = mapped_column(Enum(OTAPlatform)) 

42 # The manifest's runtimeVersion / build's expo-runtime-version. A build only accepts a manifest whose 

43 # runtimeVersion equals its own, so (platform, fingerprint) is the compatibility key. 

44 fingerprint: Mapped[str] = mapped_column(String) 

45 # The CDN path component the signed manifest is published under, e.g. v1.3.<commit>.<sha>. 

46 version: Mapped[str] = mapped_column(String) 

47 # The manifest's createdAt: the publish/stamp time used to order rollouts. A rollback rolls forward by 

48 # republishing the good bundle re-stamped with a newer createdAt so it sorts newest. 

49 manifest_created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) 

50 # Restamped to a fresh UUID on every publish — expo-updates skips updates whose id matches the 

51 # installed one, so reusing an id silently drops the publish. 

52 manifest_id: Mapped[str] = mapped_column(String) 

53 

54 # Stops handing this bundle to new check-ins; can't reclaim devices already on it (they only move 

55 # forward in createdAt), so it's a stop-gap until a re-stamped rollback is published. 

56 banned_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), default=None) 

57 banned_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), default=None) 

58 banned_reason: Mapped[str | None] = mapped_column(String, default=None) 

59 

60 creator_user: Mapped[User] = relationship(init=False, foreign_keys="OTAPackage.creator_user_id") 

61 banned_by_user: Mapped[User | None] = relationship(init=False, foreign_keys="OTAPackage.banned_by_user_id") 

62 

63 __table_args__ = ( 

64 UniqueConstraint("platform", "version", name="uq_ota_packages_platform_version"), 

65 UniqueConstraint("platform", "manifest_id", name="uq_ota_packages_platform_manifest_id"), 

66 Index("ix_ota_packages_resolve", "platform", "fingerprint", "manifest_created_at"), 

67 # All three ban columns move together: either the package isn't banned, or every audit field 

68 # is filled in. Bans are irreversible (rolled forward by republishing) so the reason is 

69 # required. 

70 CheckConstraint( 

71 "(banned_at IS NULL AND banned_by_user_id IS NULL AND banned_reason IS NULL) " 

72 "OR (banned_at IS NOT NULL AND banned_by_user_id IS NOT NULL AND banned_reason IS NOT NULL)", 

73 name="ck_ota_packages_ban_columns_consistent", 

74 ), 

75 ) 

76 

77 

78class NativeClientUser(Base, kw_only=True): 

79 # Append-only log of (eas_client_id, user_id) sightings. Each authenticated CheckNativeStatus 

80 # writes a row; the newest row for a given eas_client_id is the current user-of-record. Keeping 

81 # history (rather than upserting) lets us reconstruct who was using an install at any past time 

82 # for incident debugging, and a shared install shows up as alternating user_ids over time. 

83 __tablename__ = "native_client_users" 

84 

85 id: Mapped[int] = mapped_column(BigInteger, primary_key=True, init=False) 

86 time: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), init=False) 

87 eas_client_id: Mapped[uuid.UUID] = mapped_column(Uuid, index=True) 

88 user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True) 

89 

90 user: Mapped[User] = relationship(init=False)